diff --git a/.github/maintainers_guide.md b/.github/maintainers_guide.md index 2edf3821e..bd15836a9 100644 --- a/.github/maintainers_guide.md +++ b/.github/maintainers_guide.md @@ -15,13 +15,13 @@ All you need to work with this project is a supported version of [Node.js](https #### Unit Tests -This package has unit tests for most files in the same directory the code is in with the suffix `.spec` (i.e. `exampleFile.spec.ts`). You can run the entire test suite using the npm script `npm test`. This command is also executed by GitHub Actions, the continuous integration service, for every Pull Request and branch. The coverage is computed with the `codecov` package. The tests themselves are run using the `mocha` test runner. +This package has unit tests for most files in the same directory the code is in with the suffix `.spec` (i.e. `exampleFile.spec.ts`). You can run the entire test suite using the npm script `npm test`. This command is also executed by GitHub Actions, the continuous integration service, for every Pull Request and branch. Coverage is collected with Node.js's built-in test coverage support and uploaded by CI. The tests themselves are run using Node.js's built-in test runner. Test code should be written in syntax that runs on the oldest supported Node.js version. This ensures that backwards compatibility is tested and the APIs look reasonable in versions of Node.js that do not support the most modern syntax. #### Debugging -A useful trick for debugging inside tests is to use the Chrome Debugging Protocol feature of Node.js to set breakpoints and interactively debug. In order to do this you must run mocha directly. This means that you should have already linted the source (`npm run lint`), manually. You then run the tests using the following command: `./node_modules/.bin/mocha test/{test-name}.js --debug-brk --inspect` (replace {test-name} with an actual test file). +A useful trick for debugging inside tests is to use the Chrome Debugging Protocol feature of Node.js to set breakpoints and interactively debug. In order to do this you should have already linted the source (`npm run lint`), manually. You can then run a specific test file with Node.js's test runner, for example: `node --inspect-brk --import tsx --test test/unit/{test-name}.spec.ts` (replace `{test-name}` with an actual test file path). #### Local Development diff --git a/.vscode/launch.json b/.vscode/launch.json index 9b9055530..c1ebc81b6 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -8,14 +8,19 @@ "type": "node", "request": "launch", "name": "Spec tests", - "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", + "runtimeExecutable": "node", "stopOnEntry": false, - "args": ["--config", ".mocharc.json", "--no-timeouts", "src/*.spec.ts", "src/**/*.spec.ts"], + "args": [ + "--inspect-brk", + "--import", + "tsx", + "--test", + "test/unit/**/*.spec.ts" + ], "cwd": "${workspaceFolder}", - "runtimeExecutable": null, "env": { "NODE_ENV": "testing", - "TS_NODE_PROJECT": "tsconfig.test.json" + "TSX_TSCONFIG_PATH": "tsconfig.test.json" }, "skipFiles": ["/**"] } diff --git a/AGENTS.md b/AGENTS.md index 697b4af49..c7b4cfb88 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -31,8 +31,8 @@ npm test # Full pipeline: build -> lint -> type tests -> unit test npm run build # Clean build (rm dist/ + tsc compilation) npm run lint # Biome check (formatting + linting) npm run lint:fix # Biome auto-fix -npm run test:unit # Unit tests only (mocha) -npm run test:coverage # Unit tests with coverage (c8) +npm run test:unit # Unit tests only (Node.js test runner) +npm run test:coverage # Unit tests with built-in Node.js coverage npm run test:types # Type definition tests (tsd) npm run watch # Watch mode for development (rebuilds on src/ changes) ``` @@ -171,9 +171,9 @@ test/types/ # tsd type tests ### Test Conventions - **Test files** use `*.spec.ts` suffix -- **Assertions** use chai (`expect`, `assert`) +- **Assertions** in tests use direct `node:assert/strict` and `sinon.assert` imports - **Mocking** uses sinon (`stub`, `spy`, `fake`) and proxyquire for module-level dependency replacement -- **Test config** in `test/unit/.mocharc.json` +- **Test config** lives in `package.json` scripts plus direct `node:test` imports in the spec files - **Where to put new tests:** Mirror the source structure. For `src/Foo.ts`, add `test/unit/Foo.spec.ts`. For `src/receivers/Bar.ts`, add `test/unit/receivers/Bar.spec.ts`. ### CI diff --git a/package.json b/package.json index 12254c280..65f8f7214 100644 --- a/package.json +++ b/package.json @@ -32,8 +32,8 @@ "lint": "npx @biomejs/biome check docs src test examples", "lint:fix": "npx @biomejs/biome check --write docs src test examples", "test": "npm run build && npm run lint && npm run test:types && npm run test:coverage", - "test:unit": "TS_NODE_PROJECT=tsconfig.json mocha --config test/unit/.mocharc.json", - "test:coverage": "c8 npm run test:unit", + "test:unit": "TSX_TSCONFIG_PATH=tsconfig.test.json tsx --test test/unit/**/*.spec.ts", + "test:coverage": "TSX_TSCONFIG_PATH=tsconfig.test.json node --experimental-test-coverage --import tsx --test test/unit/**/*.spec.ts", "test:types": "tsd --files test/types", "watch": "npx nodemon --watch 'src' --ext 'ts' --exec npm run build" }, @@ -61,21 +61,15 @@ "@biomejs/biome": "^1.9.0", "@changesets/cli": "^2.29.8", "@tsconfig/node18": "^18.2.4", - "@types/chai": "^4.1.7", - "@types/mocha": "^10.0.1", "@types/node": "18.19.130", "@types/proxyquire": "^1.3.31", "@types/sinon": "^17.0.4", "@types/tsscmp": "^1.0.0", - "c8": "^10.1.2", - "chai": "~4.3.0", - "mocha": "^10.2.0", "proxyquire": "^2.1.3", "shx": "^0.3.2", "sinon": "^20.0.0", - "source-map-support": "^0.5.12", - "ts-node": "^10.9.2", "tsd": "^0.31.2", + "tsx": "^4.20.6", "typescript": "5.3.3" }, "peerDependencies": { diff --git a/test/unit/.mocharc.json b/test/unit/.mocharc.json deleted file mode 100644 index ad1427e24..000000000 --- a/test/unit/.mocharc.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "require": ["ts-node/register", "source-map-support/register"], - "spec": ["test/unit/**/*.spec.ts"], - "timeout": 10000 -} diff --git a/test/unit/App/basic.spec.ts b/test/unit/App/basic.spec.ts index b7a8ad78c..cb109830c 100644 --- a/test/unit/App/basic.spec.ts +++ b/test/unit/App/basic.spec.ts @@ -1,5 +1,5 @@ import { LogLevel } from '@slack/logger'; -import { assert } from 'chai'; +import assert from 'node:assert/strict'; import sinon from 'sinon'; import { ErrorCode } from '../../../src/errors'; import SocketModeReceiver from '../../../src/receivers/SocketModeReceiver'; @@ -17,6 +17,7 @@ import { withNoopWebClient, withSuccessfulBotUserFetchingWebClient, } from '../helpers'; +import { describe, it } from 'node:test'; const fakeAppToken = 'xapp-1234'; const fakeBotId = 'B_FAKE_BOT_ID'; @@ -34,13 +35,17 @@ describe('App basic features', () => { const MockApp = importApp(overrides); const app = new MockApp({ token: '', signingSecret: '', port: 9999 }); // biome-ignore lint/complexity/useLiteralKeys: reaching into private fields - assert.propertyVal(app['receiver'], 'port', 9999); + assert.ok(app['receiver'] && typeof app['receiver'] === 'object'); + assert.ok('port' in app['receiver']); + assert.deepStrictEqual((app['receiver'] as unknown as Record)['port'], 9999); }); it('should accept a port value under installerOptions', async () => { const MockApp = importApp(overrides); const app = new MockApp({ token: '', signingSecret: '', port: 7777, installerOptions: { port: 9999 } }); // biome-ignore lint/complexity/useLiteralKeys: reaching into private fields - assert.propertyVal(app['receiver'], 'port', 9999); + assert.ok(app['receiver'] && typeof app['receiver'] === 'object'); + assert.ok('port' in app['receiver']); + assert.deepStrictEqual((app['receiver'] as unknown as Record)['port'], 9999); }); }); @@ -65,7 +70,9 @@ describe('App basic features', () => { installationStore, }); // biome-ignore lint/complexity/useLiteralKeys: reaching into private fields - assert.propertyVal(app['receiver'], 'httpServerPort', 9999); + assert.ok(app['receiver'] && typeof app['receiver'] === 'object'); + assert.ok('httpServerPort' in app['receiver']); + assert.deepStrictEqual((app['receiver'] as unknown as Record)['httpServerPort'], 9999); }); it('should accept a port value under installerOptions', async () => { const MockApp = importApp(overrides); @@ -82,7 +89,9 @@ describe('App basic features', () => { installationStore, }); // biome-ignore lint/complexity/useLiteralKeys: reaching into private fields - assert.propertyVal(app['receiver'], 'httpServerPort', 9999); + assert.ok(app['receiver'] && typeof app['receiver'] === 'object'); + assert.ok('httpServerPort' in app['receiver']); + assert.deepStrictEqual((app['receiver'] as unknown as Record)['httpServerPort'], 9999); }); }); @@ -93,12 +102,12 @@ describe('App basic features', () => { const MockApp = importApp(overrides); const app = new MockApp({ token: '', signingSecret: '' }); // TODO: verify that the fake bot ID and fake bot user ID are retrieved - assert.instanceOf(app, MockApp); + assert.ok(app instanceof MockApp); }); it('should pass the given token to app.client', async () => { const MockApp = importApp(overrides); const app = new MockApp({ token: 'xoxb-foo-bar', signingSecret: '' }); - assert.isDefined(app.client); + assert.notStrictEqual(app.client, undefined); assert.equal(app.client.token, 'xoxb-foo-bar'); }); }); @@ -114,7 +123,9 @@ describe('App basic features', () => { new MockApp({ signingSecret: '' }); assert.fail(); } catch (error) { - assert.propertyVal(error, 'code', ErrorCode.AppInitializationError); + assert.ok(error && typeof error === 'object'); + assert.ok('code' in error); + assert.deepStrictEqual((error as unknown as Record)['code'], ErrorCode.AppInitializationError); } }); it('should fail when both a token and authorize callback are specified', async () => { @@ -124,7 +135,9 @@ describe('App basic features', () => { new MockApp({ token: '', authorize: authorizeCallback, signingSecret: '' }); assert.fail(); } catch (error) { - assert.propertyVal(error, 'code', ErrorCode.AppInitializationError); + assert.ok(error && typeof error === 'object'); + assert.ok('code' in error); + assert.deepStrictEqual((error as unknown as Record)['code'], ErrorCode.AppInitializationError); assert(authorizeCallback.notCalled); } }); @@ -135,7 +148,9 @@ describe('App basic features', () => { new MockApp({ token: '', clientId: '', clientSecret: '', stateSecret: '', signingSecret: '' }); assert.fail(); } catch (error) { - assert.propertyVal(error, 'code', ErrorCode.AppInitializationError); + assert.ok(error && typeof error === 'object'); + assert.ok('code' in error); + assert.deepStrictEqual((error as unknown as Record)['code'], ErrorCode.AppInitializationError); assert(authorizeCallback.notCalled); } }); @@ -152,7 +167,9 @@ describe('App basic features', () => { }); assert.fail(); } catch (error) { - assert.propertyVal(error, 'code', ErrorCode.AppInitializationError); + assert.ok(error && typeof error === 'object'); + assert.ok('code' in error); + assert.deepStrictEqual((error as unknown as Record)['code'], ErrorCode.AppInitializationError); assert(authorizeCallback.notCalled); } }); @@ -171,7 +188,9 @@ describe('App basic features', () => { new MockApp({ authorize: noop }); assert.fail(); } catch (error) { - assert.propertyVal(error, 'code', ErrorCode.AppInitializationError); + assert.ok(error && typeof error === 'object'); + assert.ok('code' in error); + assert.deepStrictEqual((error as unknown as Record)['code'], ErrorCode.AppInitializationError); } }); it('should fail when both socketMode and a custom receiver are specified', async () => { @@ -181,7 +200,9 @@ describe('App basic features', () => { new MockApp({ token: '', signingSecret: '', socketMode: true, receiver: fakeReceiver }); assert.fail(); } catch (error) { - assert.propertyVal(error, 'code', ErrorCode.AppInitializationError); + assert.ok(error && typeof error === 'object'); + assert.ok('code' in error); + assert.deepStrictEqual((error as unknown as Record)['code'], ErrorCode.AppInitializationError); } }); it('should succeed when both socketMode and SocketModeReceiver are specified', async () => { @@ -220,7 +241,7 @@ describe('App basic features', () => { const dummyConvoStore = createFakeConversationStore(); const MockApp = importApp(overrides); const app = new MockApp({ convoStore: dummyConvoStore, authorize: noop, signingSecret: '' }); - assert.instanceOf(app, MockApp); + assert.ok(app instanceof MockApp); assert(fakeConversationContext.firstCall.calledWith(dummyConvoStore)); }); }); @@ -231,7 +252,9 @@ describe('App basic features', () => { new MockApp({ token: '', signingSecret: '', redirectUri: 'http://example.com/redirect' }); // eslint-disable-line no-new assert.fail(); } catch (error) { - assert.propertyVal(error, 'code', ErrorCode.AppInitializationError); + assert.ok(error && typeof error === 'object'); + assert.ok('code' in error); + assert.deepStrictEqual((error as unknown as Record)['code'], ErrorCode.AppInitializationError); } }); it('should fail when missing installerOptions.redirectUriPath', async () => { @@ -245,7 +268,9 @@ describe('App basic features', () => { }); assert.fail(); } catch (error) { - assert.propertyVal(error, 'code', ErrorCode.AppInitializationError); + assert.ok(error && typeof error === 'object'); + assert.ok('code' in error); + assert.deepStrictEqual((error as unknown as Record)['code'], ErrorCode.AppInitializationError); } }); }); @@ -306,23 +331,23 @@ describe('App basic features', () => { signingSecret: 'invalid-one', deferInitialization: true, }); - assert.instanceOf(app, MockApp); + assert.ok(app instanceof MockApp); try { await app.start(); assert.fail('The start() method should fail before init() call'); } catch (err) { - assert.propertyVal( - err, - 'message', - 'This App instance is not yet initialized. Call `await App#init()` before starting the app.', - ); + assert.ok(err && typeof err === 'object'); + assert.ok('message' in err); + assert.deepStrictEqual((err as unknown as Record)['message'], 'This App instance is not yet initialized. Call `await App#init()` before starting the app.'); } try { await app.init(); assert.fail('The init() method should fail here'); } catch (err) { console.log(err); - assert.propertyVal(err, 'message', exception); + assert.ok(err && typeof err === 'object'); + assert.ok('message' in err); + assert.deepStrictEqual((err as unknown as Record)['message'], exception); } }); }); @@ -336,8 +361,12 @@ describe('App basic features', () => { const fakeLogger = createFakeLogger(); const MockApp = importApp(overrides); const app = new MockApp({ logger: fakeLogger, token: '', appToken: fakeAppToken, developerMode: true }); - assert.propertyVal(app, 'logLevel', LogLevel.DEBUG); - assert.propertyVal(app, 'socketMode', true); + assert.ok(app && typeof app === 'object'); + assert.ok('logLevel' in app); + assert.deepStrictEqual((app as unknown as Record)['logLevel'], LogLevel.DEBUG); + assert.ok(app && typeof app === 'object'); + assert.ok('socketMode' in app); + assert.deepStrictEqual((app as unknown as Record)['socketMode'], true); }); }); diff --git a/test/unit/App/middlewares/arguments.spec.ts b/test/unit/App/middlewares/arguments.spec.ts index d28b16d65..eccf95403 100644 --- a/test/unit/App/middlewares/arguments.spec.ts +++ b/test/unit/App/middlewares/arguments.spec.ts @@ -1,5 +1,5 @@ import type { WebClient } from '@slack/web-api'; -import { assert } from 'chai'; +import assert from 'node:assert/strict'; import sinon, { type SinonSpy } from 'sinon'; import { LogLevel } from '../../../../src/App'; import type { SayStreamFn } from '../../../../src/context/create-say-stream'; @@ -29,6 +29,7 @@ import { withSetStatus, withSuccessfulBotUserFetchingWebClient, } from '../../helpers'; +import { beforeEach, describe, it } from 'node:test'; describe('App middleware and listener arguments', () => { let fakeReceiver: FakeReceiver; @@ -428,7 +429,7 @@ describe('App middleware and listener arguments', () => { sinon.assert.calledThrice(fakeAck); - assert.isUndefined(app.client.token); + assert.strictEqual(app.client.token, undefined); assert.equal(clients[0].token, 'xoxb-123'); assert.equal(clients[1].token, 'xoxp-456'); assert.equal(clients[2].token, 'xoxb-123'); @@ -545,8 +546,12 @@ describe('App middleware and listener arguments', () => { // Assert that each call to fakePostMessage had the right arguments for (const call of fakePostMessage.getCalls()) { const firstArg = call.args[0]; - assert.propertyVal(firstArg, 'text', dummyMessage); - assert.propertyVal(firstArg, 'channel', dummyChannelId); + assert.ok(firstArg && typeof firstArg === 'object'); + assert.ok('text' in firstArg); + assert.deepStrictEqual((firstArg as unknown as Record)['text'], dummyMessage); + assert.ok(firstArg && typeof firstArg === 'object'); + assert.ok('channel' in firstArg); + assert.deepStrictEqual((firstArg as unknown as Record)['channel'], dummyChannelId); } sinon.assert.notCalled(fakeErrorHandler); }); @@ -572,8 +577,12 @@ describe('App middleware and listener arguments', () => { // Assert that each call to fakePostMessage had the right arguments for (const call of fakePostMessage.getCalls()) { const firstArg = call.args[0]; - assert.propertyVal(firstArg, 'channel', dummyChannelId); - assert.propertyVal(firstArg, 'text', dummyMessage.text); + assert.ok(firstArg && typeof firstArg === 'object'); + assert.ok('channel' in firstArg); + assert.deepStrictEqual((firstArg as unknown as Record)['channel'], dummyChannelId); + assert.ok(firstArg && typeof firstArg === 'object'); + assert.ok('text' in firstArg); + assert.deepStrictEqual((firstArg as unknown as Record)['text'], dummyMessage.text); } sinon.assert.notCalled(fakeErrorHandler); }); @@ -634,7 +643,7 @@ describe('App middleware and listener arguments', () => { const app = new MockApp({ receiver: fakeReceiver, authorize: sinon.fake.resolves(dummyAuthorizationResult) }); app.use(async (args) => { - assert.notProperty(args, 'say'); + assert.ok(!('say' in args)); // If the above assertion fails, then it would throw an AssertionError and the following line will not be // called assertionAggregator(); @@ -678,7 +687,7 @@ describe('App middleware and listener arguments', () => { app.use(async (args) => { // biome-ignore lint/suspicious/noExplicitAny: test utility const sayStream = (args as any).sayStream as SayStreamFn; - assert.isFunction(sayStream); + assert.strictEqual(typeof sayStream, 'function'); assertionAggregator(); }); app.error(fakeErrorHandler); @@ -742,7 +751,7 @@ describe('App middleware and listener arguments', () => { const assertionAggregator = sinon.fake(); const app = new MockApp({ receiver: fakeReceiver, authorize: sinon.fake.resolves(dummyAuthorizationResult) }); app.use(async (args) => { - assert.notProperty(args, 'sayStream'); + assert.ok(!('sayStream' in args)); assertionAggregator(); }); @@ -772,7 +781,7 @@ describe('App middleware and listener arguments', () => { app.use(async (args) => { // biome-ignore lint/suspicious/noExplicitAny: test utility const setStatus = (args as any).setStatus as SetStatusFn; - assert.isFunction(setStatus); + assert.strictEqual(typeof setStatus, 'function'); assertionAggregator(); }); app.error(fakeErrorHandler); @@ -801,7 +810,7 @@ describe('App middleware and listener arguments', () => { const assertionAggregator = sinon.fake(); const app = new MockApp({ receiver: fakeReceiver, authorize: sinon.fake.resolves(dummyAuthorizationResult) }); app.use(async (args) => { - assert.notProperty(args, 'setStatus'); + assert.ok(!('setStatus' in args)); assertionAggregator(); }); @@ -958,7 +967,7 @@ describe('App middleware and listener arguments', () => { ack: fakeAck, }); - assert.isTrue(called); + assert.strictEqual(called, true); sinon.assert.calledOnce(fakeAck); }); diff --git a/test/unit/App/middlewares/global.spec.ts b/test/unit/App/middlewares/global.spec.ts index 1f332e971..020e0af32 100644 --- a/test/unit/App/middlewares/global.spec.ts +++ b/test/unit/App/middlewares/global.spec.ts @@ -1,5 +1,5 @@ import type { WebClient } from '@slack/web-api'; -import { assert } from 'chai'; +import assert from 'node:assert/strict'; import sinon, { type SinonSpy } from 'sinon'; import type App from '../../../../src/App'; import type { ExtendedErrorHandlerArgs } from '../../../../src/App'; @@ -19,6 +19,7 @@ import { withNoopAppMetadata, withNoopWebClient, } from '../../helpers'; +import { beforeEach, describe, it } from 'node:test'; describe('App global middleware Processing', () => { let fakeReceiver: FakeReceiver; @@ -81,7 +82,7 @@ describe('App global middleware Processing', () => { assert(fakeErrorHandler.notCalled); assert(fakeMiddleware.notCalled); - assert.isAtLeast(fakeLogger.warn.callCount, invalidReceiverEvents.length); + assert.ok(fakeLogger.warn.callCount >= invalidReceiverEvents.length); }); it('should warn, send to global error handler, acknowledge, and skip when a receiver event fails authorization', async () => { @@ -103,9 +104,13 @@ describe('App global middleware Processing', () => { assert(fakeMiddleware.notCalled); assert(fakeLogger.warn.called); - assert.instanceOf(fakeErrorHandler.firstCall.args[0], Error); - assert.propertyVal(fakeErrorHandler.firstCall.args[0], 'code', ErrorCode.AuthorizationError); - assert.propertyVal(fakeErrorHandler.firstCall.args[0], 'original', dummyAuthorizationError.original); + assert.ok(fakeErrorHandler.firstCall.args[0] instanceof Error); + assert.ok(fakeErrorHandler.firstCall.args[0] && typeof fakeErrorHandler.firstCall.args[0] === 'object'); + assert.ok('code' in fakeErrorHandler.firstCall.args[0]); + assert.deepStrictEqual((fakeErrorHandler.firstCall.args[0] as unknown as Record)['code'], ErrorCode.AuthorizationError); + assert.ok(fakeErrorHandler.firstCall.args[0] && typeof fakeErrorHandler.firstCall.args[0] === 'object'); + assert.ok('original' in fakeErrorHandler.firstCall.args[0]); + assert.deepStrictEqual((fakeErrorHandler.firstCall.args[0] as unknown as Record)['original'], dummyAuthorizationError.original); assert(fakeAck.called); }); @@ -123,7 +128,7 @@ describe('App global middleware Processing', () => { await fakeReceiver.sendEvent(dummyReceiverEvent); // Assert - assert.instanceOf(fakeErrorHandler.firstCall.args[0], Error); + assert.ok(fakeErrorHandler.firstCall.args[0] instanceof Error); }); it('correctly waits for async listeners', async () => { @@ -137,7 +142,7 @@ describe('App global middleware Processing', () => { }); await fakeReceiver.sendEvent(dummyReceiverEvent); - assert.isTrue(changed); + assert.strictEqual(changed, true); assert(fakeErrorHandler.notCalled); }); @@ -216,7 +221,7 @@ describe('App global middleware Processing', () => { }); app.error(async (codedError: CodedError) => { - assert.instanceOf(codedError, UnknownError); + assert.ok(codedError instanceof UnknownError); assert.equal(codedError.message, error.message); }); @@ -233,14 +238,14 @@ describe('App global middleware Processing', () => { }); app.error(async (args: ExtendedErrorHandlerArgs) => { - assert.property(args, 'error'); - assert.property(args, 'body'); - assert.property(args, 'context'); - assert.property(args, 'logger'); - assert.isDefined(args.error); - assert.isDefined(args.body); - assert.isDefined(args.context); - assert.isDefined(args.logger); + assert.ok('error' in args); + assert.ok('body' in args); + assert.ok('context' in args); + assert.ok('logger' in args); + assert.notStrictEqual(args.error, undefined); + assert.notStrictEqual(args.body, undefined); + assert.notStrictEqual(args.context, undefined); + assert.notStrictEqual(args.logger, undefined); assert.equal(args.error.message, error.message); }); @@ -265,7 +270,7 @@ describe('App global middleware Processing', () => { actualError = err; } - assert.instanceOf(actualError, UnknownError); + assert.ok(actualError instanceof UnknownError); assert.equal(actualError.message, error.message); }); @@ -283,7 +288,7 @@ describe('App global middleware Processing', () => { const testData = createDummyCustomFunctionMiddlewareArgs({ options: { autoAcknowledge: false } }); await fakeReceiver.sendEvent({ ack: fakeAck, body: testData.body }); - assert.isDefined(clientArg); + assert.notStrictEqual(clientArg, undefined); assert.equal(clientArg.token, 'xwfp-valid'); }); @@ -302,7 +307,7 @@ describe('App global middleware Processing', () => { const testData = createDummyCustomFunctionMiddlewareArgs({ options: { autoAcknowledge: false } }); await fakeReceiver.sendEvent({ ack: fakeAck, body: testData.body }); - assert.isDefined(clientArg); + assert.notStrictEqual(clientArg, undefined); assert.equal(clientArg.token, undefined); }); @@ -320,11 +325,11 @@ describe('App global middleware Processing', () => { const testData = createDummyCustomFunctionMiddlewareArgs({ options: { autoAcknowledge: false } }); await fakeReceiver.sendEvent({ ack: fakeAck, body: testData.body }); - assert.isDefined(clientArg); + assert.notStrictEqual(clientArg, undefined); assert.equal(clientArg.token, 'xwfp-valid'); await fakeReceiver.sendEvent(dummyReceiverEvent); - assert.isDefined(clientArg); + assert.notStrictEqual(clientArg, undefined); assert.equal(clientArg.token, 'xoxb-valid'); }); }); diff --git a/test/unit/App/middlewares/ignore-self.spec.ts b/test/unit/App/middlewares/ignore-self.spec.ts index 4f6d68217..9e5c88f96 100644 --- a/test/unit/App/middlewares/ignore-self.spec.ts +++ b/test/unit/App/middlewares/ignore-self.spec.ts @@ -15,6 +15,7 @@ import { withNoopAppMetadata, withNoopWebClient, } from '../../helpers'; +import { beforeEach, describe, it } from 'node:test'; function buildOverrides(secondOverrides: Override[]): Override { return mergeOverrides( diff --git a/test/unit/App/middlewares/listener.spec.ts b/test/unit/App/middlewares/listener.spec.ts index f6c4815ba..0a92cd50e 100644 --- a/test/unit/App/middlewares/listener.spec.ts +++ b/test/unit/App/middlewares/listener.spec.ts @@ -1,8 +1,9 @@ -import { assert } from 'chai'; +import assert from 'node:assert/strict'; import sinon, { type SinonSpy } from 'sinon'; import type App from '../../../../src/App'; import { ErrorCode, isCodedError } from '../../../../src/errors'; import { FakeReceiver, createDummyReceiverEvent, importApp } from '../../helpers'; +import { beforeEach, describe, it } from 'node:test'; describe('App listener middleware processing', () => { let fakeReceiver: FakeReceiver; @@ -56,8 +57,8 @@ describe('App listener middleware processing', () => { const error = fakeErrorHandler.firstCall.args[0]; assert.ok(isCodedError(error)); assert(error.code === ErrorCode.MultipleListenerError); - assert.isArray(error.originals); - if (error.originals) assert.sameMembers(error.originals, errorsToThrow); + assert.ok(Array.isArray(error.originals)); + if (error.originals) assert.deepStrictEqual([...error.originals].sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b))), [...errorsToThrow].sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b)))); }); // https://github.com/slackapi/bolt-js/issues/1457 diff --git a/test/unit/App/routing-action.spec.ts b/test/unit/App/routing-action.spec.ts index 5415e7468..d166b3b53 100644 --- a/test/unit/App/routing-action.spec.ts +++ b/test/unit/App/routing-action.spec.ts @@ -1,4 +1,4 @@ -import { assert } from 'chai'; +import assert from 'node:assert/strict'; import sinon, { type SinonSpy } from 'sinon'; import type App from '../../../src/App'; import { @@ -15,6 +15,7 @@ import { withNoopAppMetadata, withNoopWebClient, } from '../helpers'; +import { beforeEach, describe, it } from 'node:test'; function buildOverrides(secondOverrides: Override[]): Override { return mergeOverrides( @@ -118,8 +119,8 @@ describe('App action() routing', () => { const testInputs = { test: true }; const testHandler = sinon.spy(async ({ inputs, complete, fail, client }) => { assert.equal(inputs, testInputs); - assert.typeOf(complete, 'function'); - assert.typeOf(fail, 'function'); + assert.strictEqual(typeof complete, 'function'); + assert.strictEqual(typeof fail, 'function'); assert.equal(client.token, 'xwfp-valid'); }); app.action('my_id', testHandler); diff --git a/test/unit/App/routing-assistant.spec.ts b/test/unit/App/routing-assistant.spec.ts index 2d6c96863..e1174f09a 100644 --- a/test/unit/App/routing-assistant.spec.ts +++ b/test/unit/App/routing-assistant.spec.ts @@ -16,6 +16,7 @@ import { withNoopAppMetadata, withNoopWebClient, } from '../helpers'; +import { beforeEach, describe, it } from 'node:test'; function buildOverrides(secondOverrides: Override[]): Override { return mergeOverrides( diff --git a/test/unit/App/routing-command.spec.ts b/test/unit/App/routing-command.spec.ts index 69467ae86..1103a394f 100644 --- a/test/unit/App/routing-command.spec.ts +++ b/test/unit/App/routing-command.spec.ts @@ -13,6 +13,7 @@ import { withNoopAppMetadata, withNoopWebClient, } from '../helpers'; +import { beforeEach, describe, it } from 'node:test'; function buildOverrides(secondOverrides: Override[]): Override { return mergeOverrides( diff --git a/test/unit/App/routing-event.spec.ts b/test/unit/App/routing-event.spec.ts index bae907b7f..344a8d297 100644 --- a/test/unit/App/routing-event.spec.ts +++ b/test/unit/App/routing-event.spec.ts @@ -14,6 +14,7 @@ import { withNoopAppMetadata, withNoopWebClient, } from '../helpers'; +import { beforeEach, describe, it } from 'node:test'; function buildOverrides(secondOverrides: Override[]): Override { return mergeOverrides( diff --git a/test/unit/App/routing-function.spec.ts b/test/unit/App/routing-function.spec.ts index 90db27f40..f640b4884 100644 --- a/test/unit/App/routing-function.spec.ts +++ b/test/unit/App/routing-function.spec.ts @@ -1,4 +1,4 @@ -import { assert } from 'chai'; +import assert from 'node:assert/strict'; import sinon from 'sinon'; import type App from '../../../src/App'; import { @@ -14,6 +14,7 @@ import { withNoopAppMetadata, withNoopWebClient, } from '../helpers'; +import { beforeEach, describe, it } from 'node:test'; function buildOverrides(secondOverrides: Override[]): Override { return mergeOverrides( @@ -65,8 +66,8 @@ describe('App function() routing', () => { const testInputs = { test: true }; const testHandler = sinon.spy(async ({ inputs, complete, fail, client }) => { assert.equal(inputs, testInputs); - assert.typeOf(complete, 'function'); - assert.typeOf(fail, 'function'); + assert.strictEqual(typeof complete, 'function'); + assert.strictEqual(typeof fail, 'function'); assert.equal(client.token, 'xwfp-valid'); }); app.function('my_id', testHandler); diff --git a/test/unit/App/routing-message.spec.ts b/test/unit/App/routing-message.spec.ts index b85013fcf..b37c775ea 100644 --- a/test/unit/App/routing-message.spec.ts +++ b/test/unit/App/routing-message.spec.ts @@ -13,6 +13,7 @@ import { withNoopAppMetadata, withNoopWebClient, } from '../helpers'; +import { beforeEach, describe, it } from 'node:test'; function buildOverrides(secondOverrides: Override[]): Override { return mergeOverrides( diff --git a/test/unit/App/routing-options.spec.ts b/test/unit/App/routing-options.spec.ts index 598727611..16be2b8a8 100644 --- a/test/unit/App/routing-options.spec.ts +++ b/test/unit/App/routing-options.spec.ts @@ -13,6 +13,7 @@ import { withNoopAppMetadata, withNoopWebClient, } from '../helpers'; +import { beforeEach, describe, it } from 'node:test'; function buildOverrides(secondOverrides: Override[]): Override { return mergeOverrides( diff --git a/test/unit/App/routing-shortcut.spec.ts b/test/unit/App/routing-shortcut.spec.ts index ce546dc10..3609fd1c2 100644 --- a/test/unit/App/routing-shortcut.spec.ts +++ b/test/unit/App/routing-shortcut.spec.ts @@ -13,6 +13,7 @@ import { withNoopAppMetadata, withNoopWebClient, } from '../helpers'; +import { beforeEach, describe, it } from 'node:test'; function buildOverrides(secondOverrides: Override[]): Override { return mergeOverrides( diff --git a/test/unit/App/routing-view.spec.ts b/test/unit/App/routing-view.spec.ts index 5f10d1262..ad9ae2844 100644 --- a/test/unit/App/routing-view.spec.ts +++ b/test/unit/App/routing-view.spec.ts @@ -14,6 +14,7 @@ import { withNoopAppMetadata, withNoopWebClient, } from '../helpers'; +import { beforeEach, describe, it } from 'node:test'; function buildOverrides(secondOverrides: Override[]): Override { return mergeOverrides( diff --git a/test/unit/Assistant.spec.ts b/test/unit/Assistant.spec.ts index 6818bae5c..d2d9b555b 100644 --- a/test/unit/Assistant.spec.ts +++ b/test/unit/Assistant.spec.ts @@ -1,7 +1,7 @@ import path from 'node:path'; import type { AssistantThreadStartedEvent } from '@slack/types'; import type { WebClient } from '@slack/web-api'; -import { assert } from 'chai'; +import assert from 'node:assert/strict'; import sinon from 'sinon'; import { type AllAssistantMiddlewareArgs, @@ -24,6 +24,7 @@ import { wrapMiddleware, } from './helpers'; import { team } from './helpers/events'; +import { describe, it } from 'node:test'; function importAssistant(overrides: Override = {}): typeof import('../../src/Assistant') { const absolutePath = path.resolve(__dirname, '../../src/Assistant'); @@ -48,12 +49,12 @@ describe('Assistant class', () => { describe('constructor', () => { it('should accept config as single functions', async () => { const assistant = new Assistant(MOCK_CONFIG_SINGLE); - assert.isNotNull(assistant); + assert.notStrictEqual(assistant, null); }); it('should accept config as multiple functions', async () => { const assistant = new Assistant(MOCK_CONFIG_MULTIPLE); - assert.isNotNull(assistant); + assert.notStrictEqual(assistant, null); }); describe('validate', () => { @@ -133,7 +134,7 @@ describe('Assistant class', () => { it('should return false if not a recognized assistant event', async () => { const fakeMessageArgs = wrapMiddleware(createDummyAppMentionEventMiddlewareArgs()); const { isAssistantEvent } = importAssistant(); - assert.isFalse(isAssistantEvent(fakeMessageArgs)); + assert.strictEqual(isAssistantEvent(fakeMessageArgs), false); }); }); @@ -148,7 +149,7 @@ describe('Assistant class', () => { const fakeMessageArgs = wrapMiddleware(createDummyMessageEventMiddlewareArgs()); const { matchesConstraints } = importAssistant(); // casting here as we intentionally are providing type-mismatched argument as a runtime test - assert.isFalse(matchesConstraints(fakeMessageArgs as unknown as AssistantMiddlewareArgs)); + assert.strictEqual(matchesConstraints(fakeMessageArgs as unknown as AssistantMiddlewareArgs), false); }); it('should return true if not message event', async () => { @@ -168,19 +169,19 @@ describe('Assistant class', () => { it('should return false if not correct subtype', async () => { const fakeMessageArgs = wrapMiddleware(createDummyMessageEventMiddlewareArgs({ thread_ts: '1234.56' })); const { isAssistantMessage } = importAssistant(); - assert.isFalse(isAssistantMessage(fakeMessageArgs.payload)); + assert.strictEqual(isAssistantMessage(fakeMessageArgs.payload), false); }); it('should return false if thread_ts is missing', async () => { const fakeMessageArgs = wrapMiddleware(createDummyMessageEventMiddlewareArgs()); const { isAssistantMessage } = importAssistant(); - assert.isFalse(isAssistantMessage(fakeMessageArgs.payload)); + assert.strictEqual(isAssistantMessage(fakeMessageArgs.payload), false); }); it('should return false if channel_type is incorrect', async () => { const fakeMessageArgs = wrapMiddleware(createDummyMessageEventMiddlewareArgs({ channel_type: 'mpim' })); const { isAssistantMessage } = importAssistant(); - assert.isFalse(isAssistantMessage(fakeMessageArgs.payload)); + assert.strictEqual(isAssistantMessage(fakeMessageArgs.payload), false); }); }); }); @@ -201,9 +202,9 @@ describe('Assistant class', () => { const threadContextChangedArgs = enrichAssistantArgs(mockThreadContextStore, mockThreadContextChangedArgs); const userMessageArgs = enrichAssistantArgs(mockThreadContextStore, mockUserMessageArgs); - assert.notExists(threadStartedArgs.next); - assert.notExists(threadContextChangedArgs.next); - assert.notExists(userMessageArgs.next); + assert.equal(threadStartedArgs.next ?? null, null); + assert.equal(threadContextChangedArgs.next ?? null, null); + assert.equal(userMessageArgs.next ?? null, null); }); it('should augment assistant_thread_started args with utilities', async () => { @@ -215,11 +216,11 @@ describe('Assistant class', () => { payload, } as AllAssistantMiddlewareArgs); - assert.exists(assistantArgs.say); - assert.exists(assistantArgs.setStatus); - assert.exists(assistantArgs.sayStream); - assert.exists(assistantArgs.setSuggestedPrompts); - assert.exists(assistantArgs.setTitle); + assert.notEqual(assistantArgs.say ?? null, null); + assert.notEqual(assistantArgs.setStatus ?? null, null); + assert.notEqual(assistantArgs.sayStream ?? null, null); + assert.notEqual(assistantArgs.setSuggestedPrompts ?? null, null); + assert.notEqual(assistantArgs.setTitle ?? null, null); }); it('should augment assistant_thread_context_changed args with utilities', async () => { @@ -231,11 +232,11 @@ describe('Assistant class', () => { payload, } as AllAssistantMiddlewareArgs); - assert.exists(assistantArgs.say); - assert.exists(assistantArgs.setStatus); - assert.exists(assistantArgs.sayStream); - assert.exists(assistantArgs.setSuggestedPrompts); - assert.exists(assistantArgs.setTitle); + assert.notEqual(assistantArgs.say ?? null, null); + assert.notEqual(assistantArgs.setStatus ?? null, null); + assert.notEqual(assistantArgs.sayStream ?? null, null); + assert.notEqual(assistantArgs.setSuggestedPrompts ?? null, null); + assert.notEqual(assistantArgs.setTitle ?? null, null); }); it('should augment message args with utilities', async () => { @@ -247,11 +248,11 @@ describe('Assistant class', () => { payload, } as AllAssistantMiddlewareArgs); - assert.exists(assistantArgs.say); - assert.exists(assistantArgs.setStatus); - assert.exists(assistantArgs.sayStream); - assert.exists(assistantArgs.setSuggestedPrompts); - assert.exists(assistantArgs.setTitle); + assert.notEqual(assistantArgs.say ?? null, null); + assert.notEqual(assistantArgs.setStatus ?? null, null); + assert.notEqual(assistantArgs.sayStream ?? null, null); + assert.notEqual(assistantArgs.setSuggestedPrompts ?? null, null); + assert.notEqual(assistantArgs.setTitle ?? null, null); }); describe('extractThreadInfo', () => { @@ -288,7 +289,13 @@ describe('Assistant class', () => { assert.equal(payload.channel, channelId); // @ts-expect-error TODO: AssistantUserMessageMiddlewareArgs extends from too broad of a message event type, which contains types that explicitly DO NOT have a thread_ts. this is at odds with the expectation around assistant user message events. assert.equal(payload.thread_ts, threadTs); - assert.isEmpty(context); + if (Array.isArray(context) || typeof context === 'string') { + assert.strictEqual(context.length, 0); + } else if (context && typeof context === 'object') { + assert.strictEqual(Object.keys(context).length, 0); + } else { + assert.fail('expected value to be empty'); + } }); it('should throw error if `channel_id` or `thread_ts` are missing', async () => { diff --git a/test/unit/AssistantThreadContextStore.spec.ts b/test/unit/AssistantThreadContextStore.spec.ts index d1453c332..def1aa34d 100644 --- a/test/unit/AssistantThreadContextStore.spec.ts +++ b/test/unit/AssistantThreadContextStore.spec.ts @@ -1,9 +1,10 @@ import type { WebClient } from '@slack/web-api'; -import { assert } from 'chai'; +import assert from 'node:assert/strict'; import sinon from 'sinon'; import { extractThreadInfo } from '../../src/Assistant'; import { DefaultThreadContextStore } from '../../src/AssistantThreadContextStore'; import { createDummyAssistantThreadStartedEventMiddlewareArgs, wrapMiddleware } from './helpers'; +import { describe, it } from 'node:test'; describe('DefaultThreadContextStore class', () => { describe('get', () => { @@ -42,7 +43,13 @@ describe('DefaultThreadContextStore class', () => { mockThreadStartedArgs.client = fakeClient as unknown as WebClient; const threadContext = await mockContextStore.get(mockThreadStartedArgs); - assert.isEmpty(threadContext); + if (Array.isArray(threadContext) || typeof threadContext === 'string') { + assert.strictEqual(threadContext.length, 0); + } else if (threadContext && typeof threadContext === 'object') { + assert.strictEqual(Object.keys(threadContext).length, 0); + } else { + assert.fail('expected value to be empty'); + } }); it('should return an empty object if no message metadata exists', async () => { @@ -64,7 +71,13 @@ describe('DefaultThreadContextStore class', () => { mockThreadStartedArgs.client = fakeClient as unknown as WebClient; const threadContext = await mockContextStore.get(mockThreadStartedArgs); - assert.isEmpty(threadContext); + if (Array.isArray(threadContext) || typeof threadContext === 'string') { + assert.strictEqual(threadContext.length, 0); + } else if (threadContext && typeof threadContext === 'object') { + assert.strictEqual(Object.keys(threadContext).length, 0); + } else { + assert.fail('expected value to be empty'); + } }); it('should retrieve instance context if it has been saved previously', async () => { diff --git a/test/unit/CustomFunction.spec.ts b/test/unit/CustomFunction.spec.ts index 5aa123579..b451f954c 100644 --- a/test/unit/CustomFunction.spec.ts +++ b/test/unit/CustomFunction.spec.ts @@ -1,4 +1,4 @@ -import { assert } from 'chai'; +import assert from 'node:assert/strict'; import { CustomFunction, type SlackCustomFunctionMiddlewareArgs, @@ -8,6 +8,7 @@ import { import { CustomFunctionInitializationError } from '../../src/errors'; import { autoAcknowledge, matchEventType, onlyEvents } from '../../src/middleware/builtin'; import type { Middleware } from '../../src/types'; +import { describe, it } from 'node:test'; const MOCK_FN = async () => {}; const MOCK_FN_2 = async () => {}; @@ -19,12 +20,12 @@ describe('CustomFunction', () => { describe('constructor', () => { it('should accept single function as middleware', async () => { const fn = new CustomFunction('test_callback_id', MOCK_MIDDLEWARE_SINGLE, { autoAcknowledge: true }); - assert.isNotNull(fn); + assert.notStrictEqual(fn, null); }); it('should accept multiple functions as middleware', async () => { const fn = new CustomFunction('test_callback_id', MOCK_MIDDLEWARE_MULTIPLE, { autoAcknowledge: true }); - assert.isNotNull(fn); + assert.notStrictEqual(fn, null); }); }); @@ -45,7 +46,7 @@ describe('CustomFunction', () => { const cbId = 'test_executed_callback_id'; const fn = new CustomFunction(cbId, MOCK_MIDDLEWARE_SINGLE, { autoAcknowledge: false }); const listeners = fn.getListeners(); - assert.isFalse(listeners.includes(autoAcknowledge)); + assert.strictEqual(listeners.includes(autoAcknowledge), false); }); }); diff --git a/test/unit/WorkflowStep.spec.ts b/test/unit/WorkflowStep.spec.ts index dab9cc663..c899438fa 100644 --- a/test/unit/WorkflowStep.spec.ts +++ b/test/unit/WorkflowStep.spec.ts @@ -1,6 +1,6 @@ import path from 'node:path'; import type { WebClient } from '@slack/web-api'; -import { assert } from 'chai'; +import assert from 'node:assert/strict'; import sinon from 'sinon'; import { type AllWorkflowStepMiddlewareArgs, @@ -15,6 +15,7 @@ import { import { WorkflowStepInitializationError } from '../../src/errors'; import type { AllMiddlewareArgs, AnyMiddlewareArgs, Middleware, WorkflowStepEdit } from '../../src/types'; import { type Override, noopVoid, proxyquire } from './helpers'; +import { describe, it } from 'node:test'; function importWorkflowStep(overrides: Override = {}): typeof import('../../src/WorkflowStep') { const absolutePath = path.resolve(__dirname, '../../src/WorkflowStep'); @@ -37,12 +38,12 @@ describe('WorkflowStep class', () => { describe('constructor', () => { it('should accept config as single functions', async () => { const ws = new WorkflowStep('test_callback_id', MOCK_CONFIG_SINGLE); - assert.isNotNull(ws); + assert.notStrictEqual(ws, null); }); it('should accept config as multiple functions', async () => { const ws = new WorkflowStep('test_callback_id', MOCK_CONFIG_MULTIPLE); - assert.isNotNull(ws); + assert.notStrictEqual(ws, null); }); }); @@ -152,9 +153,9 @@ describe('WorkflowStep class', () => { const viewIsStepEvent = isStepEvent(fakeSaveArgs); const executeIsStepEvent = isStepEvent(fakeExecuteArgs); - assert.isTrue(editIsStepEvent); - assert.isTrue(viewIsStepEvent); - assert.isTrue(executeIsStepEvent); + assert.strictEqual(editIsStepEvent, true); + assert.strictEqual(viewIsStepEvent, true); + assert.strictEqual(executeIsStepEvent, true); }); it('should return false if not a recognized workflow step payload type', async () => { @@ -164,7 +165,7 @@ describe('WorkflowStep class', () => { const { isStepEvent } = importWorkflowStep(); const actionIsStepEvent = isStepEvent(fakeEditArgs); - assert.isFalse(actionIsStepEvent); + assert.strictEqual(actionIsStepEvent, false); }); }); @@ -181,9 +182,9 @@ describe('WorkflowStep class', () => { const viewStepArgs = prepareStepArgs(fakeSaveArgs); const executeStepArgs = prepareStepArgs(fakeExecuteArgs); - assert.notExists(editStepArgs.next); - assert.notExists(viewStepArgs.next); - assert.notExists(executeStepArgs.next); + assert.equal(editStepArgs.next ?? null, null); + assert.equal(viewStepArgs.next ?? null, null); + assert.equal(executeStepArgs.next ?? null, null); }); it('should augment workflow_step_edit args with step and configure()', async () => { @@ -192,8 +193,8 @@ describe('WorkflowStep class', () => { // casting to returned type because prepareStepArgs isn't built to do so const stepArgs = prepareStepArgs(fakeArgs as AllWorkflowStepMiddlewareArgs); - assert.exists(stepArgs.step); - assert.property(stepArgs, 'configure'); + assert.notEqual(stepArgs.step ?? null, null); + assert.ok('configure' in stepArgs); }); it('should augment view_submission with step and update()', async () => { @@ -204,8 +205,8 @@ describe('WorkflowStep class', () => { fakeArgs as unknown as AllWorkflowStepMiddlewareArgs, ); - assert.exists(stepArgs.step); - assert.property(stepArgs, 'update'); + assert.notEqual(stepArgs.step ?? null, null); + assert.ok('update' in stepArgs); }); it('should augment workflow_step_execute with step, complete() and fail()', async () => { @@ -216,9 +217,9 @@ describe('WorkflowStep class', () => { fakeArgs as unknown as AllWorkflowStepMiddlewareArgs, ); - assert.exists(stepArgs.step); - assert.property(stepArgs, 'complete'); - assert.property(stepArgs, 'fail'); + assert.notEqual(stepArgs.step ?? null, null); + assert.ok('complete' in stepArgs); + assert.ok('fail' in stepArgs); }); }); diff --git a/test/unit/context/create-function-complete.spec.ts b/test/unit/context/create-function-complete.spec.ts index c96b6503f..b4a58e1b3 100644 --- a/test/unit/context/create-function-complete.spec.ts +++ b/test/unit/context/create-function-complete.spec.ts @@ -1,7 +1,8 @@ import { WebClient } from '@slack/web-api'; -import { assert } from 'chai'; +import assert from 'node:assert/strict'; import sinon from 'sinon'; import { createFunctionComplete } from '../../../src/context'; +import { describe, it } from 'node:test'; describe('createFunctionComplete', () => { it('complete should call functions.completeSuccess', async () => { @@ -28,9 +29,9 @@ describe('createFunctionComplete', () => { sinon.stub(client.functions, 'completeSuccess').resolves(); const complete = createFunctionComplete({ isEnterpriseInstall: false, functionExecutionId: 'Fx1234' }, client); - assert.isFalse(complete.hasBeenCalled(), 'hasBeenCalled should be false initially'); + assert.strictEqual(complete.hasBeenCalled(), false, 'hasBeenCalled should be false initially'); await complete(); - assert.isTrue(complete.hasBeenCalled(), 'hasBeenCalled should be true after invoking complete'); + assert.strictEqual(complete.hasBeenCalled(), true, 'hasBeenCalled should be true after invoking complete'); }); }); diff --git a/test/unit/context/create-function-fail.spec.ts b/test/unit/context/create-function-fail.spec.ts index 41c04ec71..68cf32447 100644 --- a/test/unit/context/create-function-fail.spec.ts +++ b/test/unit/context/create-function-fail.spec.ts @@ -1,7 +1,8 @@ import { WebClient } from '@slack/web-api'; -import { assert } from 'chai'; +import assert from 'node:assert/strict'; import sinon from 'sinon'; import { createFunctionFail } from '../../../src/context'; +import { describe, it } from 'node:test'; describe('createFunctionFail', () => { it('fail should call functions.completeError', async () => { @@ -28,9 +29,9 @@ describe('createFunctionFail', () => { sinon.stub(client.functions, 'completeError').resolves(); const fail = createFunctionFail({ isEnterpriseInstall: false, functionExecutionId: 'Fx1234' }, client); - assert.isFalse(fail.hasBeenCalled(), 'hasBeenCalled should be false initially'); + assert.strictEqual(fail.hasBeenCalled(), false, 'hasBeenCalled should be false initially'); await fail({ error: 'boom' }); - assert.isTrue(fail.hasBeenCalled(), 'hasBeenCalled should be true after calling the function'); + assert.strictEqual(fail.hasBeenCalled(), true, 'hasBeenCalled should be true after calling the function'); }); }); diff --git a/test/unit/context/create-respond.spec.ts b/test/unit/context/create-respond.spec.ts index a24a37e57..aea2ed974 100644 --- a/test/unit/context/create-respond.spec.ts +++ b/test/unit/context/create-respond.spec.ts @@ -1,7 +1,8 @@ import type { AxiosInstance } from 'axios'; -import { assert } from 'chai'; +import assert from 'node:assert/strict'; import sinon from 'sinon'; import { createRespond } from '../../../src/context'; +import { describe, it } from 'node:test'; describe('createRespond', () => { it('should post to the response URL with text when given a string', async () => { diff --git a/test/unit/context/create-say-stream.spec.ts b/test/unit/context/create-say-stream.spec.ts index b7a9010c6..ba99fee19 100644 --- a/test/unit/context/create-say-stream.spec.ts +++ b/test/unit/context/create-say-stream.spec.ts @@ -1,8 +1,9 @@ import { WebClient } from '@slack/web-api'; -import { assert } from 'chai'; +import assert from 'node:assert/strict'; import sinon from 'sinon'; import { createSayStream } from '../../../src/context'; import type { Context } from '../../../src/types'; +import { afterEach, beforeEach, describe, it } from 'node:test'; describe('createSayStream', () => { const sandbox = sinon.createSandbox(); diff --git a/test/unit/context/create-say.spec.ts b/test/unit/context/create-say.spec.ts index 006e23b40..6cdef3a51 100644 --- a/test/unit/context/create-say.spec.ts +++ b/test/unit/context/create-say.spec.ts @@ -1,7 +1,8 @@ import { WebClient } from '@slack/web-api'; -import { assert } from 'chai'; +import assert from 'node:assert/strict'; import sinon from 'sinon'; import { createSay } from '../../../src/context'; +import { describe, it } from 'node:test'; describe('createSay', () => { it('should call chat.postMessage with text when given a string', async () => { diff --git a/test/unit/context/create-set-status.spec.ts b/test/unit/context/create-set-status.spec.ts index 4709e19d0..c55a50787 100644 --- a/test/unit/context/create-set-status.spec.ts +++ b/test/unit/context/create-set-status.spec.ts @@ -1,7 +1,8 @@ import { WebClient } from '@slack/web-api'; -import { assert } from 'chai'; +import assert from 'node:assert/strict'; import sinon from 'sinon'; import { createSetStatus } from '../../../src/context'; +import { afterEach, beforeEach, describe, it } from 'node:test'; describe('createSetStatus', () => { const sandbox = sinon.createSandbox(); diff --git a/test/unit/conversation-store.spec.ts b/test/unit/conversation-store.spec.ts index e677c0ed5..77977d34f 100644 --- a/test/unit/conversation-store.spec.ts +++ b/test/unit/conversation-store.spec.ts @@ -1,10 +1,11 @@ import path from 'node:path'; import type { Logger } from '@slack/logger'; import type { WebClient } from '@slack/web-api'; -import { assert, AssertionError } from 'chai'; +import assert, { AssertionError } from 'node:assert/strict'; import sinon, { type SinonSpy } from 'sinon'; import type { AnyMiddlewareArgs, Context, NextFn } from '../../src/types'; import { type Override, createFakeLogger, delay, proxyquire } from './helpers'; +import { describe, it } from 'node:test'; /* Testing Harness */ @@ -69,8 +70,8 @@ describe('conversationContext middleware', () => { // Assert assert(fakeLogger.debug.called); assert(fakeNext.called); - assert.notProperty(dummyContext, 'updateConversation'); - assert.notProperty(dummyContext, 'conversation'); + assert.ok(!('updateConversation' in dummyContext)); + assert.ok(!('conversation' in dummyContext)); }); it('should add to the context for events within a conversation that was not previously stored and pass expiresAt', async () => { @@ -100,7 +101,7 @@ describe('conversationContext middleware', () => { await middleware(fakeArgs); assert(fakeNext.called); - assert.notProperty(dummyContext, 'conversation'); + assert.ok(!('conversation' in dummyContext)); if (dummyContext.updateConversation === undefined) { assert.fail(); } @@ -136,8 +137,8 @@ describe('conversationContext middleware', () => { // Assert assert(fakeNext.called); - assert.notProperty(dummyContext, 'conversation'); - // NOTE: chai types do not offer assertion signatures yet, and neither do node's assert module types. + assert.ok(!('conversation' in dummyContext)); + // NOTE: node:assert types do not offer assertion signatures here. if (dummyContext.updateConversation === undefined) { assert.fail(); } @@ -173,7 +174,7 @@ describe('conversationContext middleware', () => { // Assert assert.equal(dummyContext.conversation, dummyConversationState); - // NOTE: chai types do not offer assertion signatures yet, and neither do node's assert module types. + // NOTE: node:assert types do not offer assertion signatures here. if (dummyContext.updateConversation === undefined) { assert.fail(); } @@ -195,7 +196,7 @@ describe('MemoryStore', () => { const store = new MemoryStore(); // Assert - assert.isOk(store); + assert.ok(store); }); }); @@ -229,8 +230,8 @@ describe('MemoryStore', () => { assert.fail(); } catch (error) { // Assert - assert.instanceOf(error, Error); - assert.notInstanceOf(error, AssertionError); + assert.ok(error instanceof Error); + assert.ok(!(error instanceof AssertionError)); } }); @@ -250,8 +251,8 @@ describe('MemoryStore', () => { assert.fail(); } catch (error) { // Assert - assert.instanceOf(error, Error); - assert.notInstanceOf(error, AssertionError); + assert.ok(error instanceof Error); + assert.ok(!(error instanceof AssertionError)); } }); }); diff --git a/test/unit/errors.spec.ts b/test/unit/errors.spec.ts index ec907eaa2..d07050495 100644 --- a/test/unit/errors.spec.ts +++ b/test/unit/errors.spec.ts @@ -1,4 +1,4 @@ -import { assert } from 'chai'; +import assert from 'node:assert/strict'; import { AppInitializationError, AuthorizationError, @@ -10,6 +10,7 @@ import { UnknownError, asCodedError, } from '../../src/errors'; +import { describe, it } from 'node:test'; describe('Errors', () => { it('has errors matching codes', () => { @@ -28,7 +29,7 @@ describe('Errors', () => { }); it('wraps non-coded errors', () => { - assert.instanceOf(asCodedError(new Error('AHH!')), UnknownError); + assert.ok(asCodedError(new Error('AHH!')) instanceof UnknownError); }); it('passes coded errors through', () => { diff --git a/test/unit/helpers.spec.ts b/test/unit/helpers.spec.ts index 688d23069..8ce202062 100644 --- a/test/unit/helpers.spec.ts +++ b/test/unit/helpers.spec.ts @@ -1,4 +1,4 @@ -import { assert } from 'chai'; +import assert from 'node:assert/strict'; import { IncomingEventType, extractEventChannelId, @@ -11,6 +11,7 @@ import { isRecord, } from '../../src/helpers'; import type { AnyMiddlewareArgs, KnownEventFromType, ReceiverEvent, SlackEventMiddlewareArgs } from '../../src/types'; +import { describe, it } from 'node:test'; describe('Helpers', () => { describe('getTypeAndConversation()', () => { @@ -116,7 +117,13 @@ describe('Helpers', () => { const typeAndConversation = getTypeAndConversation(fakeEventBody); // Assert - assert.isEmpty(typeAndConversation); + if (Array.isArray(typeAndConversation) || typeof typeAndConversation === 'string') { + assert.strictEqual(typeAndConversation.length, 0); + } else if (typeAndConversation && typeof typeAndConversation === 'object') { + assert.strictEqual(Object.keys(typeAndConversation).length, 0); + } else { + assert.fail('expected value to be empty'); + } }); }); }); @@ -236,47 +243,47 @@ describe('Helpers', () => { }); describe(`${isRecord.name}()`, () => { it('should return true for plain objects', () => { - assert.isTrue(isRecord({})); - assert.isTrue(isRecord({ key: 'value' })); + assert.strictEqual(isRecord({}), true); + assert.strictEqual(isRecord({ key: 'value' }), true); }); it('should return true for arrays', () => { - assert.isTrue(isRecord([])); + assert.strictEqual(isRecord([]), true); }); it('should return false for null', () => { - assert.isFalse(isRecord(null)); + assert.strictEqual(isRecord(null), false); }); it('should return false for undefined', () => { - assert.isFalse(isRecord(undefined)); + assert.strictEqual(isRecord(undefined), false); }); it('should return false for primitives', () => { - assert.isFalse(isRecord('string')); - assert.isFalse(isRecord(42)); - assert.isFalse(isRecord(true)); + assert.strictEqual(isRecord('string'), false); + assert.strictEqual(isRecord(42), false); + assert.strictEqual(isRecord(true), false); }); }); describe(`${hasStringProperty.name}()`, () => { it('should return true when key exists with a string value', () => { - assert.isTrue(hasStringProperty({ name: 'test' }, 'name')); + assert.strictEqual(hasStringProperty({ name: 'test' }, 'name'), true); }); it('should return false when key exists with a non-string value', () => { - assert.isFalse(hasStringProperty({ count: 42 }, 'count')); - assert.isFalse(hasStringProperty({ flag: true }, 'flag')); - assert.isFalse(hasStringProperty({ nested: {} }, 'nested')); + assert.strictEqual(hasStringProperty({ count: 42 }, 'count'), false); + assert.strictEqual(hasStringProperty({ flag: true }, 'flag'), false); + assert.strictEqual(hasStringProperty({ nested: {} }, 'nested'), false); }); it('should return false when key does not exist', () => { - assert.isFalse(hasStringProperty({ other: 'value' }, 'missing')); + assert.strictEqual(hasStringProperty({ other: 'value' }, 'missing'), false); }); it('should return false for null or undefined input', () => { - assert.isFalse(hasStringProperty(null, 'key')); - assert.isFalse(hasStringProperty(undefined, 'key')); + assert.strictEqual(hasStringProperty(null, 'key'), false); + assert.strictEqual(hasStringProperty(undefined, 'key'), false); }); }); @@ -329,7 +336,7 @@ describe('Helpers', () => { for (const { name, event } of noThreadTsEvents) { it(`should return undefined for ${name}`, () => { - assert.isUndefined(extractEventThreadTs(event as KnownEventFromType)); + assert.strictEqual(extractEventThreadTs(event as KnownEventFromType), undefined); }); } }); @@ -357,7 +364,7 @@ describe('Helpers', () => { for (const { name, event } of noTsEvents) { it(`should return undefined for ${name}`, () => { - assert.isUndefined(extractEventTs(event as KnownEventFromType)); + assert.strictEqual(extractEventTs(event as KnownEventFromType), undefined); }); } }); @@ -408,7 +415,7 @@ describe('Helpers', () => { for (const { name, event } of noChannelEvents) { it(`should return undefined for ${name}`, () => { - assert.isUndefined(extractEventChannelId(event as KnownEventFromType)); + assert.strictEqual(extractEventChannelId(event as KnownEventFromType), undefined); }); } diff --git a/test/unit/middleware/builtin.spec.ts b/test/unit/middleware/builtin.spec.ts index 9d9422531..dde0897cc 100644 --- a/test/unit/middleware/builtin.spec.ts +++ b/test/unit/middleware/builtin.spec.ts @@ -1,5 +1,5 @@ import path from 'node:path'; -import { assert } from 'chai'; +import assert from 'node:assert/strict'; import sinon from 'sinon'; import { expectType } from 'tsd'; import { ErrorCode } from '../../../src/errors'; @@ -16,6 +16,7 @@ import { proxyquire, wrapMiddleware, } from '../helpers'; +import { beforeEach, describe, it } from 'node:test'; interface DummyContext extends Context { matches?: RegExpExecArray; @@ -40,7 +41,7 @@ describe('Built-in global middleware', () => { function matchesPatternTestCase( pattern: string | RegExp, event: SlackEventMiddlewareArgs<'message' | 'app_mention'>, - ): Mocha.AsyncFunc { + ): () => Promise { return async () => { const { matchMessage } = builtins; const middleware = matchMessage(pattern); @@ -52,7 +53,7 @@ describe('Built-in global middleware', () => { // The following assertion(s) check behavior that is only targeted at RegExp patterns if (typeof pattern !== 'string') { if (ctx.matches !== undefined) { - assert.lengthOf(ctx.matches, 1); + assert.strictEqual(ctx.matches.length, 1); } else { assert.fail(); } @@ -63,7 +64,7 @@ describe('Built-in global middleware', () => { function notMatchesPatternTestCase( pattern: string | RegExp, event: SlackEventMiddlewareArgs<'message' | 'app_mention'>, - ): Mocha.AsyncFunc { + ): () => Promise { return async () => { const { matchMessage } = builtins; const middleware = matchMessage(pattern); @@ -72,7 +73,7 @@ describe('Built-in global middleware', () => { await middleware(args); sinon.assert.notCalled(args.next); - assert.notProperty(ctx, 'matches'); + assert.ok(!('matches' in ctx)); }; } @@ -161,9 +162,13 @@ describe('Built-in global middleware', () => { error = err as Error; } - assert.instanceOf(error, Error); - assert.propertyVal(error, 'code', ErrorCode.ContextMissingPropertyError); - assert.propertyVal(error, 'missingProperty', 'botUserId'); + assert.ok(error instanceof Error); + assert.ok(error && typeof error === 'object'); + assert.ok('code' in error); + assert.deepStrictEqual((error as unknown as Record)['code'], ErrorCode.ContextMissingPropertyError); + assert.ok(error && typeof error === 'object'); + assert.ok('missingProperty' in error); + assert.deepStrictEqual((error as unknown as Record)['missingProperty'], 'botUserId'); }); it('should match message events that mention the bot user ID at the beginning of message text', async () => { @@ -417,7 +422,7 @@ describe('Built-in global middleware', () => { describe(isSlackEventMiddlewareArgsOptions.name, () => { it('should return true if object is SlackEventMiddlewareArgsOptions', async () => { const actual = isSlackEventMiddlewareArgsOptions({ autoAcknowledge: true }); - assert.isTrue(actual); + assert.strictEqual(actual, true); }); it('should narrow proper type if object is SlackEventMiddlewareArgsOptions', async () => { @@ -431,7 +436,7 @@ describe('Built-in global middleware', () => { it('should return false if object is Middleware', async () => { const actual = isSlackEventMiddlewareArgsOptions(async () => {}); - assert.isFalse(actual); + assert.strictEqual(actual, false); }); }); }); diff --git a/test/unit/receivers/AwsLambdaReceiver.spec.ts b/test/unit/receivers/AwsLambdaReceiver.spec.ts index ba14c3fc6..a7ad9e6d1 100644 --- a/test/unit/receivers/AwsLambdaReceiver.spec.ts +++ b/test/unit/receivers/AwsLambdaReceiver.spec.ts @@ -1,5 +1,5 @@ import crypto from 'node:crypto'; -import { assert } from 'chai'; +import assert from 'node:assert/strict'; import sinon from 'sinon'; import AwsLambdaReceiver from '../../../src/receivers/AwsLambdaReceiver'; import { @@ -12,6 +12,7 @@ import { withNoopAppMetadata, withNoopWebClient, } from '../helpers'; +import { describe, it } from 'node:test'; const fakeAuthTestResponse = { ok: true, @@ -30,7 +31,7 @@ describe('AwsLambdaReceiver', () => { signingSecret: 'my-secret', logger: noopLogger, }); - assert.isNotNull(awsReceiver); + assert.notStrictEqual(awsReceiver, null); }); it('should have start method', async () => { @@ -39,7 +40,7 @@ describe('AwsLambdaReceiver', () => { logger: noopLogger, }); const startedHandler = await awsReceiver.start(); - assert.isNotNull(startedHandler); + assert.notStrictEqual(startedHandler, null); }); it('should have stop method', async () => { diff --git a/test/unit/receivers/ExpressReceiver.spec.ts b/test/unit/receivers/ExpressReceiver.spec.ts index f5ec2e41f..fdad444a0 100644 --- a/test/unit/receivers/ExpressReceiver.spec.ts +++ b/test/unit/receivers/ExpressReceiver.spec.ts @@ -3,7 +3,7 @@ import type { Server as HTTPSServer } from 'node:https'; import path from 'node:path'; import { Readable } from 'node:stream'; import type { InstallProvider } from '@slack/oauth'; -import { assert } from 'chai'; +import assert from 'node:assert/strict'; import type { Application, IRouter, Request, Response } from 'express'; import sinon, { type SinonFakeTimers } from 'sinon'; import App from '../../../src/App'; @@ -19,7 +19,6 @@ import ExpressReceiver, { verifySignatureAndParseRawBody, buildBodyParserMiddleware, } from '../../../src/receivers/ExpressReceiver'; -import * as httpFunc from '../../../src/receivers/HTTPModuleFunctions'; import type { ReceiverEvent } from '../../../src/types'; import { FakeServer, @@ -30,6 +29,7 @@ import { withHttpCreateServer, withHttpsCreateServer, } from '../helpers'; +import { afterEach, beforeEach, describe, it } from 'node:test'; // Loading the system under test using overrides function importExpressReceiver( @@ -86,7 +86,7 @@ describe('ExpressReceiver', () => { }, customPropertiesExtractor: (req) => ({ headers: req.headers }), }); - assert.isNotNull(receiver); + assert.notStrictEqual(receiver, null); }); it('should accept custom Express app / router', async () => { const app = { @@ -113,7 +113,7 @@ describe('ExpressReceiver', () => { app: app as unknown as Application, router: router as unknown as IRouter, }); - assert.isNotNull(receiver); + assert.notStrictEqual(receiver, null); sinon.assert.calledOnce(app.use); sinon.assert.calledOnce(router.get); sinon.assert.calledOnce(router.post); @@ -138,10 +138,9 @@ describe('ExpressReceiver', () => { redirectUri, installerOptions, }); - assert.isNotNull(receiver); + assert.notStrictEqual(receiver, null); // missing redirectUriPath - assert.throws( - () => + assert.throws(() => new ExpressReceiver({ clientId, clientSecret, @@ -149,12 +148,9 @@ describe('ExpressReceiver', () => { stateSecret, scopes, redirectUri, - }), - AppInitializationError, - ); + }), AppInitializationError); // inconsistent redirectUriPath - assert.throws( - () => + assert.throws(() => new ExpressReceiver({ clientId: 'my-clientId', clientSecret, @@ -165,12 +161,9 @@ describe('ExpressReceiver', () => { installerOptions: { redirectUriPath: '/hiya', }, - }), - AppInitializationError, - ); + }), AppInitializationError); // inconsistent redirectUri - assert.throws( - () => + assert.throws(() => new ExpressReceiver({ clientId: 'my-clientId', clientSecret, @@ -179,9 +172,7 @@ describe('ExpressReceiver', () => { scopes, redirectUri: 'http://example.com/hiya', installerOptions, - }), - AppInitializationError, - ); + }), AppInitializationError); }); }); @@ -230,7 +221,7 @@ describe('ExpressReceiver', () => { caughtError = error as Error; } - assert.instanceOf(caughtError, Error); + assert.ok(caughtError instanceof Error); }); it('should reject with an error when the built-in HTTP server returns undefined', async () => { const fakeCreateUndefinedServer = sinon.fake.returns(undefined); @@ -249,8 +240,10 @@ describe('ExpressReceiver', () => { caughtError = error as Error; } - assert.instanceOf(caughtError, ReceiverInconsistentStateError); - assert.propertyVal(caughtError, 'code', ErrorCode.ReceiverInconsistentStateError); + assert.ok(caughtError instanceof ReceiverInconsistentStateError); + assert.ok(caughtError && typeof caughtError === 'object'); + assert.ok('code' in caughtError); + assert.deepStrictEqual((caughtError as unknown as Record)['code'], ErrorCode.ReceiverInconsistentStateError); }); it('should reject with an error when starting and the server was already previously started', async () => { const ER = importExpressReceiver(overrides); @@ -265,8 +258,10 @@ describe('ExpressReceiver', () => { caughtError = error as Error; } - assert.instanceOf(caughtError, ReceiverInconsistentStateError); - assert.propertyVal(caughtError, 'code', ErrorCode.ReceiverInconsistentStateError); + assert.ok(caughtError instanceof ReceiverInconsistentStateError); + assert.ok(caughtError && typeof caughtError === 'object'); + assert.ok('code' in caughtError); + assert.deepStrictEqual((caughtError as unknown as Record)['code'], ErrorCode.ReceiverInconsistentStateError); }); }); @@ -290,8 +285,10 @@ describe('ExpressReceiver', () => { caughtError = error as Error; } - assert.instanceOf(caughtError, ReceiverInconsistentStateError); - assert.propertyVal(caughtError, 'code', ErrorCode.ReceiverInconsistentStateError); + assert.ok(caughtError instanceof ReceiverInconsistentStateError); + assert.ok(caughtError && typeof caughtError === 'object'); + assert.ok('code' in caughtError); + assert.deepStrictEqual((caughtError as unknown as Record)['code'], ErrorCode.ReceiverInconsistentStateError); }); it('should reject when a built-in HTTP server raises an error when closing', async () => { fakeServer = new FakeServer( @@ -314,16 +311,16 @@ describe('ExpressReceiver', () => { caughtError = error as Error; } - assert.instanceOf(caughtError, Error); + assert.ok(caughtError instanceof Error); assert.equal(caughtError?.message, 'this error will be raised by the underlying HTTP server during close()'); }); }); describe('#requestHandler()', () => { - const extractRetryNumStub = sinon.stub(httpFunc, 'extractRetryNumFromHTTPRequest'); - const extractRetryReasonStub = sinon.stub(httpFunc, 'extractRetryReasonFromHTTPRequest'); - const buildNoBodyResponseStub = sinon.stub(httpFunc, 'buildNoBodyResponse'); - const buildContentResponseStub = sinon.stub(httpFunc, 'buildContentResponse'); + let extractRetryNumStub: sinon.SinonStub; + let extractRetryReasonStub: sinon.SinonStub; + let buildNoBodyResponseStub: sinon.SinonStub; + let buildContentResponseStub: sinon.SinonStub; const processStub = sinon.stub<[ReceiverEvent]>().resolves({}); const ackStub = function ackStub() {}; ackStub.prototype.bind = function () { @@ -331,21 +328,27 @@ describe('ExpressReceiver', () => { }; ackStub.prototype.ack = sinon.spy(); beforeEach(() => { + extractRetryNumStub = sinon.stub().returns(undefined); + extractRetryReasonStub = sinon.stub().returns(undefined); + buildNoBodyResponseStub = sinon.stub().returns(undefined); + buildContentResponseStub = sinon.stub().returns(undefined); overrides = mergeOverrides( withHttpCreateServer(fakeCreateServer), withHttpsCreateServer(sinon.fake.throws('Should not be used.')), { './HTTPResponseAck': { HTTPResponseAck: ackStub } }, + { + './HTTPModuleFunctions': { + extractRetryNumFromHTTPRequest: extractRetryNumStub, + extractRetryReasonFromHTTPRequest: extractRetryReasonStub, + buildNoBodyResponse: buildNoBodyResponseStub, + buildContentResponse: buildContentResponseStub, + }, + }, ); }); afterEach(() => { sinon.reset(); }); - after(() => { - extractRetryNumStub.restore(); - extractRetryReasonStub.restore(); - buildNoBodyResponseStub.restore(); - buildContentResponseStub.restore(); - }); it('should not build an HTTP response if processBeforeResponse=false', async () => { const ER = importExpressReceiver(overrides); const receiver = new ER({ signingSecret: '' }); @@ -612,21 +615,21 @@ describe('ExpressReceiver', () => { // biome-ignore lint/suspicious/noExplicitAny: errors can be anything const state: any = {}; await runWithValidRequest(buildExpressRequest(), state); - assert.isUndefined(state.error); + assert.strictEqual(state.error, undefined); }); it('should verify requests on GCP', async () => { // biome-ignore lint/suspicious/noExplicitAny: errors can be anything const state: any = {}; await runWithValidRequest(buildGCPRequest(), state); - assert.isUndefined(state.error); + assert.strictEqual(state.error, undefined); }); it('should verify requests on GCP using async signingSecret', async () => { // biome-ignore lint/suspicious/noExplicitAny: errors can be anything const state: any = {}; await runWithValidRequest(buildGCPRequest(), state, () => Promise.resolve(signingSecret)); - assert.isUndefined(state.error); + assert.strictEqual(state.error, undefined); }); // ---------------------------- diff --git a/test/unit/receivers/HTTPModuleFunctions.spec.ts b/test/unit/receivers/HTTPModuleFunctions.spec.ts index 7db177f25..883501178 100644 --- a/test/unit/receivers/HTTPModuleFunctions.spec.ts +++ b/test/unit/receivers/HTTPModuleFunctions.spec.ts @@ -1,7 +1,8 @@ import { createHmac } from 'node:crypto'; import { IncomingMessage, ServerResponse } from 'node:http'; -import { assert } from 'chai'; +import assert from 'node:assert/strict'; import sinon from 'sinon'; +import { describe, it } from 'node:test'; import { AuthorizationError, HTTPReceiverDeferredRequestError, ReceiverMultipleAckError } from '../../../src/errors'; import type { BufferedIncomingMessage } from '../../../src/receivers/BufferedIncomingMessage'; @@ -14,7 +15,7 @@ describe('HTTPModuleFunctions', async () => { it('should work when the header does not exist', async () => { const req = sinon.createStubInstance(IncomingMessage) as IncomingMessage; const result = func.extractRetryNumFromHTTPRequest(req); - assert.isUndefined(result); + assert.strictEqual(result, undefined); }); it('should parse a single value header', async () => { const req = sinon.createStubInstance(IncomingMessage) as IncomingMessage; @@ -33,7 +34,7 @@ describe('HTTPModuleFunctions', async () => { it('should work when the header does not exist', async () => { const req = sinon.createStubInstance(IncomingMessage) as IncomingMessage; const result = func.extractRetryReasonFromHTTPRequest(req); - assert.isUndefined(result); + assert.strictEqual(result, undefined); }); it('should parse a valid header', async () => { const req = sinon.createStubInstance(IncomingMessage) as IncomingMessage; @@ -77,7 +78,7 @@ describe('HTTPModuleFunctions', async () => { func.getHeader(req, 'Cookie'); assert.fail('Error should be thrown here'); } catch (e) { - assert.isTrue((e as Error).message.length > 0); + assert.strictEqual((e as Error).message.length > 0, true); } }); it('should parse a valid header', async () => { @@ -106,7 +107,7 @@ describe('HTTPModuleFunctions', async () => { } as unknown as BufferedIncomingMessage; const res = sinon.createStubInstance(ServerResponse) as unknown as ServerResponse; const result = await func.parseAndVerifyHTTPRequest({ signingSecret }, req, res); - assert.isDefined(result.rawBody); + assert.notStrictEqual(result.rawBody, undefined); }); it('should detect an invalid timestamp', async () => { const signingSecret = 'secret'; @@ -127,11 +128,9 @@ describe('HTTPModuleFunctions', async () => { try { await func.parseAndVerifyHTTPRequest({ signingSecret }, req, res); } catch (e) { - assert.propertyVal( - e, - 'message', - 'Failed to verify authenticity: x-slack-request-timestamp must differ from system time by no more than 5 minutes or request is stale', - ); + assert.ok(e && typeof e === 'object'); + assert.ok('message' in e); + assert.deepStrictEqual((e as unknown as Record)['message'], 'Failed to verify authenticity: x-slack-request-timestamp must differ from system time by no more than 5 minutes or request is stale'); } }); it('should detect an invalid signature', async () => { @@ -150,7 +149,9 @@ describe('HTTPModuleFunctions', async () => { try { await func.parseAndVerifyHTTPRequest({ signingSecret }, req, res); } catch (e) { - assert.propertyVal(e, 'message', 'Failed to verify authenticity: signature mismatch'); + assert.ok(e && typeof e === 'object'); + assert.ok('message' in e); + assert.deepStrictEqual((e as unknown as Record)['message'], 'Failed to verify authenticity: signature mismatch'); } }); it('should parse a ssl_check request body without signature verification', async () => { @@ -164,7 +165,7 @@ describe('HTTPModuleFunctions', async () => { } as unknown as BufferedIncomingMessage; const res: ServerResponse = sinon.createStubInstance(ServerResponse) as unknown as ServerResponse; const result = await func.parseAndVerifyHTTPRequest({ signingSecret }, req, res); - assert.isDefined(result.rawBody); + assert.notStrictEqual(result.rawBody, undefined); }); it('should detect invalid signature for application/x-www-form-urlencoded body', async () => { const signingSecret = 'secret'; @@ -182,7 +183,9 @@ describe('HTTPModuleFunctions', async () => { try { await func.parseAndVerifyHTTPRequest({ signingSecret }, req, res); } catch (e) { - assert.propertyVal(e, 'message', 'Failed to verify authenticity: signature mismatch'); + assert.ok(e && typeof e === 'object'); + assert.ok('message' in e); + assert.deepStrictEqual((e as unknown as Record)['message'], 'Failed to verify authenticity: signature mismatch'); } }); }); @@ -192,24 +195,24 @@ describe('HTTPModuleFunctions', async () => { it('should have buildContentResponse', async () => { const res = sinon.createStubInstance(ServerResponse); func.buildContentResponse(res as unknown as ServerResponse, 'OK'); - assert.isTrue(res.writeHead.calledWith(200)); + assert.strictEqual(res.writeHead.calledWith(200), true); }); it('should have buildNoBodyResponse', async () => { const res = sinon.createStubInstance(ServerResponse); func.buildNoBodyResponse(res as unknown as ServerResponse, 500); - assert.isTrue(res.writeHead.calledWith(500)); + assert.strictEqual(res.writeHead.calledWith(500), true); }); it('should have buildSSLCheckResponse', async () => { const res = sinon.createStubInstance(ServerResponse); func.buildSSLCheckResponse(res as unknown as ServerResponse); - assert.isTrue(res.writeHead.calledWith(200)); + assert.strictEqual(res.writeHead.calledWith(200), true); }); it('should have buildUrlVerificationResponse', async () => { const res = sinon.createStubInstance(ServerResponse); func.buildUrlVerificationResponse(res as unknown as ServerResponse, { challenge: '3eZbrw1aBm2rZgRNFdxV2595E9CY3gmdALWMmHkvFXO7tYXAYM8P', }); - assert.isTrue(res.writeHead.calledWith(200)); + assert.strictEqual(res.writeHead.calledWith(200), true); }); }); @@ -226,7 +229,7 @@ describe('HTTPModuleFunctions', async () => { request, response: response as unknown as ServerResponse, }); - assert.isTrue(response.writeHead.calledWith(500)); + assert.strictEqual(response.writeHead.calledWith(500), true); }); it('should properly handle HTTPReceiverDeferredRequestError', async () => { const request = sinon.createStubInstance(IncomingMessage) as IncomingMessage; @@ -237,7 +240,7 @@ describe('HTTPModuleFunctions', async () => { request, response: response as unknown as ServerResponse, }); - assert.isTrue(response.writeHead.calledWith(404)); + assert.strictEqual(response.writeHead.calledWith(404), true); }); }); @@ -252,7 +255,7 @@ describe('HTTPModuleFunctions', async () => { request, response: response as unknown as ServerResponse, }); - assert.isTrue(response.writeHead.calledWith(500)); + assert.strictEqual(response.writeHead.calledWith(500), true); }); it('should properly handle AuthorizationError', async () => { const request = sinon.createStubInstance(IncomingMessage) as IncomingMessage; @@ -264,7 +267,7 @@ describe('HTTPModuleFunctions', async () => { request, response: response as unknown as ServerResponse, }); - assert.isTrue(response.writeHead.calledWith(401)); + assert.strictEqual(response.writeHead.calledWith(401), true); }); }); @@ -277,7 +280,7 @@ describe('HTTPModuleFunctions', async () => { request, response: response as unknown as ServerResponse, }); - assert.isTrue(response.writeHead.calledWith(404)); + assert.strictEqual(response.writeHead.calledWith(404), true); }); }); }); diff --git a/test/unit/receivers/HTTPReceiver.spec.ts b/test/unit/receivers/HTTPReceiver.spec.ts index 3d9ab1b43..3740d5a19 100644 --- a/test/unit/receivers/HTTPReceiver.spec.ts +++ b/test/unit/receivers/HTTPReceiver.spec.ts @@ -1,7 +1,7 @@ import { IncomingMessage, ServerResponse } from 'node:http'; import path from 'node:path'; import { InstallProvider } from '@slack/oauth'; -import { assert } from 'chai'; +import assert from 'node:assert/strict'; import type { ParamsDictionary } from 'express-serve-static-core'; import { match } from 'path-to-regexp'; import sinon from 'sinon'; @@ -21,6 +21,7 @@ import { withHttpCreateServer, withHttpsCreateServer, } from '../helpers'; +import { beforeEach, describe, it } from 'node:test'; // Loading the system under test using overrides function importHTTPReceiver(overrides: Override = {}): typeof import('../../../src/receivers/HTTPReceiver').default { @@ -86,7 +87,7 @@ describe('HTTPReceiver', () => { }, unhandledRequestTimeoutMillis: 2000, // the default is 3001 }); - assert.isNotNull(receiver); + assert.notStrictEqual(receiver, null); }); it('should accept a custom port', async () => { @@ -95,15 +96,19 @@ describe('HTTPReceiver', () => { const defaultPort = new HTTPReceiver({ signingSecret: 'secret', }); - assert.isNotNull(defaultPort); - assert.propertyVal(defaultPort, 'port', 3000); + assert.notStrictEqual(defaultPort, null); + assert.ok(defaultPort && typeof defaultPort === 'object'); + assert.ok('port' in defaultPort); + assert.deepStrictEqual((defaultPort as unknown as Record)['port'], 3000); const customPort = new HTTPReceiver({ port: 9999, signingSecret: 'secret', }); - assert.isNotNull(customPort); - assert.propertyVal(customPort, 'port', 9999); + assert.notStrictEqual(customPort, null); + assert.ok(customPort && typeof customPort === 'object'); + assert.ok('port' in customPort); + assert.deepStrictEqual((customPort as unknown as Record)['port'], 9999); const customPort2 = new HTTPReceiver({ port: 7777, @@ -112,8 +117,10 @@ describe('HTTPReceiver', () => { port: 9999, }, }); - assert.isNotNull(customPort2); - assert.propertyVal(customPort2, 'port', 9999); + assert.notStrictEqual(customPort2, null); + assert.ok(customPort2 && typeof customPort2 === 'object'); + assert.ok('port' in customPort2); + assert.deepStrictEqual((customPort2 as unknown as Record)['port'], 9999); }); it('should throw an error if redirect uri options supplied invalid or incomplete', async () => { @@ -137,10 +144,9 @@ describe('HTTPReceiver', () => { redirectUri, installerOptions, }); - assert.isNotNull(receiver); + assert.notStrictEqual(receiver, null); // redirectUri supplied, but missing redirectUriPath - assert.throws( - () => + assert.throws(() => new HTTPReceiver({ clientId, clientSecret, @@ -148,12 +154,9 @@ describe('HTTPReceiver', () => { stateSecret, scopes, redirectUri, - }), - AppInitializationError, - ); + }), AppInitializationError); // inconsistent redirectUriPath - assert.throws( - () => + assert.throws(() => new HTTPReceiver({ clientId: 'my-clientId', clientSecret, @@ -164,12 +167,9 @@ describe('HTTPReceiver', () => { installerOptions: { redirectUriPath: '/hiya', }, - }), - AppInitializationError, - ); + }), AppInitializationError); // inconsistent redirectUri - assert.throws( - () => + assert.throws(() => new HTTPReceiver({ clientId: 'my-clientId', clientSecret, @@ -178,9 +178,7 @@ describe('HTTPReceiver', () => { scopes, redirectUri: 'http://example.com/hiya', installerOptions, - }), - AppInitializationError, - ); + }), AppInitializationError); }); }); describe('start() method', () => { @@ -190,8 +188,10 @@ describe('HTTPReceiver', () => { const defaultPort = new HTTPReceiver({ signingSecret: 'secret', }); - assert.isNotNull(defaultPort); - assert.propertyVal(defaultPort, 'port', 3000); + assert.notStrictEqual(defaultPort, null); + assert.ok(defaultPort && typeof defaultPort === 'object'); + assert.ok('port' in defaultPort); + assert.deepStrictEqual((defaultPort as unknown as Record)['port'], 3000); await defaultPort.start(9001); sinon.assert.calledWithMatch(fakeServer.listen, sinon.match(9001)); await defaultPort.stop(); @@ -224,7 +224,7 @@ describe('HTTPReceiver', () => { userScopes, }, }); - assert.isNotNull(receiver); + assert.notStrictEqual(receiver, null); receiver.installer = installProviderStub as unknown as InstallProvider; const fakeReq = sinon.createStubInstance(IncomingMessage) as IncomingMessage; fakeReq.url = '/hiya'; @@ -256,7 +256,7 @@ describe('HTTPReceiver', () => { userScopes, }, }); - assert.isNotNull(receiver); + assert.notStrictEqual(receiver, null); receiver.installer = installProviderStub as unknown as InstallProvider; const fakeReq: IncomingMessage = sinon.createStubInstance(IncomingMessage) as IncomingMessage; fakeReq.url = '/hiya'; @@ -288,7 +288,7 @@ describe('HTTPReceiver', () => { userScopes, }, }); - assert.isNotNull(receiver); + assert.notStrictEqual(receiver, null); receiver.installer = installProviderStub as unknown as InstallProvider; const fakeReq: IncomingMessage = sinon.createStubInstance(IncomingMessage) as IncomingMessage; fakeReq.url = '/hiya'; @@ -326,7 +326,7 @@ describe('HTTPReceiver', () => { userScopes, }, }); - assert.isNotNull(receiver); + assert.notStrictEqual(receiver, null); receiver.installer = installProviderStub as unknown as InstallProvider; const fakeReq = sinon.createStubInstance(IncomingMessage) as IncomingMessage; fakeReq.url = '/heyo'; @@ -370,7 +370,7 @@ describe('HTTPReceiver', () => { userScopes, }, }); - assert.isNotNull(receiver); + assert.notStrictEqual(receiver, null); receiver.installer = installProviderStub as unknown as InstallProvider; const fakeReq = sinon.createStubInstance(IncomingMessage) as IncomingMessage; fakeReq.url = '/heyo'; @@ -559,15 +559,12 @@ describe('HTTPReceiver', () => { const HTTPReceiver = importHTTPReceiver(); const customRoutes = [{ path: '/test' }] as CustomRoute[]; - assert.throws( - () => + assert.throws(() => new HTTPReceiver({ clientSecret: 'my-client-secret', signingSecret: 'secret', customRoutes, - }), - CustomRouteInitializationError, - ); + }), CustomRouteInitializationError); }); }); @@ -598,7 +595,7 @@ describe('HTTPReceiver', () => { customRoutes, }); - assert.isNotNull(receiver); + assert.notStrictEqual(receiver, null); receiver.installer = installProviderStub as unknown as InstallProvider; const fakeReq = sinon.createStubInstance(IncomingMessage) as IncomingMessage; diff --git a/test/unit/receivers/HTTPResponseAck.spec.ts b/test/unit/receivers/HTTPResponseAck.spec.ts index 5a2cd3de6..feee9af8b 100644 --- a/test/unit/receivers/HTTPResponseAck.spec.ts +++ b/test/unit/receivers/HTTPResponseAck.spec.ts @@ -1,12 +1,18 @@ import { IncomingMessage, ServerResponse } from 'node:http'; -import { assert } from 'chai'; +import path from 'node:path'; +import assert from 'node:assert/strict'; import sinon from 'sinon'; import { expectType } from 'tsd'; import { ReceiverMultipleAckError } from '../../../src/errors'; -import * as HTTPModuleFunctions from '../../../src/receivers/HTTPModuleFunctions'; import { HTTPResponseAck } from '../../../src/receivers/HTTPResponseAck'; import type { ResponseAck } from '../../../src/types'; -import { createFakeLogger } from '../helpers'; +import { createFakeLogger, proxyquire } from '../helpers'; +import { afterEach, beforeEach, describe, it } from 'node:test'; + +function importHTTPResponseAck(overrides = {}): typeof import('../../../src/receivers/HTTPResponseAck') { + const absolutePath = path.resolve(__dirname, '../../../src/receivers/HTTPResponseAck'); + return proxyquire(absolutePath, overrides); +} describe('HTTPResponseAck', async () => { let setTimeoutSpy: sinon.SinonSpy; @@ -30,8 +36,8 @@ describe('HTTPResponseAck', async () => { httpRequest, httpResponse, }); - assert.isDefined(responseAck); - assert.isDefined(responseAck.bind()); + assert.notStrictEqual(responseAck, undefined); + assert.notStrictEqual(responseAck.bind(), undefined); expectType(responseAck); responseAck.ack(); // no exception }); @@ -46,13 +52,9 @@ describe('HTTPResponseAck', async () => { }); responseAck.ack(); // no exception assert(setTimeoutSpy.calledOnce, 'unhandledRequestHandler is set as a timeout callback exactly once'); - assert.equal( - setTimeoutSpy.firstCall.args[1], - 3001, - 'a 3 seconds timeout for the unhandledRequestHandler callback is expected', - ); + assert.equal(setTimeoutSpy.firstCall.args[1], 3001, 'a 3 seconds timeout for the unhandledRequestHandler callback is expected'); }); - it('should trigger unhandledRequestHandler if unacknowledged', (done) => { + it('should trigger unhandledRequestHandler if unacknowledged', async () => { const httpRequest = sinon.createStubInstance(IncomingMessage) as IncomingMessage; const httpResponse: ServerResponse = sinon.createStubInstance(ServerResponse) as unknown as ServerResponse; const unhandledRequestTimeoutMillis = 1; @@ -65,17 +67,11 @@ describe('HTTPResponseAck', async () => { httpRequest, httpResponse, }); - assert.equal( - setTimeoutSpy.firstCall.args[1], - unhandledRequestTimeoutMillis, - `a ${unhandledRequestTimeoutMillis} timeout for the unhandledRequestHandler callback is expected`, - ); - setTimeout(() => { - assert(spy.calledOnce); - done(); - }, 2); + assert.equal(setTimeoutSpy.firstCall.args[1], unhandledRequestTimeoutMillis, `a ${unhandledRequestTimeoutMillis} timeout for the unhandledRequestHandler callback is expected`); + await new Promise((resolve) => setTimeout(resolve, 2)); + assert(spy.calledOnce); }); - it('should not trigger unhandledRequestHandler if acknowledged', (done) => { + it('should not trigger unhandledRequestHandler if acknowledged', async () => { const httpRequest = sinon.createStubInstance(IncomingMessage) as IncomingMessage; const httpResponse: ServerResponse = sinon.createStubInstance(ServerResponse) as unknown as ServerResponse; const spy = sinon.spy(); @@ -88,10 +84,8 @@ describe('HTTPResponseAck', async () => { httpResponse, }); responseAck.ack(); - setTimeout(() => { - assert(spy.notCalled); - done(); - }, 2); + await new Promise((resolve) => setTimeout(resolve, 2)); + assert(spy.notCalled); }); it('should throw an error if a bound Ack invocation was already acknowledged', async () => { const httpRequest = sinon.createStubInstance(IncomingMessage) as IncomingMessage; @@ -108,7 +102,7 @@ describe('HTTPResponseAck', async () => { await bound(); assert.fail('No exception raised'); } catch (e) { - assert.instanceOf(e, ReceiverMultipleAckError); + assert.ok(e instanceof ReceiverMultipleAckError); } }); it('should store response body if processBeforeResponse=true', async () => { @@ -137,17 +131,18 @@ describe('HTTPResponseAck', async () => { const bound = responseAck.bind(); const body = false; await bound(body); - assert.equal( - responseAck.storedResponse, - '', - 'Falsy body passed to bound handler not stored as empty string in Ack instance.', - ); + assert.equal(responseAck.storedResponse, '', 'Falsy body passed to bound handler not stored as empty string in Ack instance.'); }); it('should call buildContentResponse with response body if processBeforeResponse=false', async () => { - const stub = sinon.stub(HTTPModuleFunctions, 'buildContentResponse'); + const stub = sinon.stub(); + const { HTTPResponseAck: HTTPResponseAckWithStub } = importHTTPResponseAck({ + './HTTPModuleFunctions': { + buildContentResponse: stub, + }, + }); const httpRequest = sinon.createStubInstance(IncomingMessage) as IncomingMessage; const httpResponse: ServerResponse = sinon.createStubInstance(ServerResponse) as unknown as ServerResponse; - const responseAck = new HTTPResponseAck({ + const responseAck = new HTTPResponseAckWithStub({ logger: createFakeLogger(), processBeforeResponse: false, httpRequest, @@ -172,11 +167,7 @@ describe('HTTPResponseAck', async () => { httpResponse, }); responseAck.ack(); // no exception - assert.equal( - setTimeoutSpy.firstCall.args[1], - 5001, - 'a 5 second timeout for the unhandledRequestHandler callback is expected', - ); + assert.equal(setTimeoutSpy.firstCall.args[1], 5001, 'a 5 second timeout for the unhandledRequestHandler callback is expected'); }); it('should not use extended timeout, when the httpRequestBody is malformed', async () => { const httpRequest = sinon.createStubInstance(IncomingMessage) as IncomingMessage; @@ -189,10 +180,6 @@ describe('HTTPResponseAck', async () => { httpResponse, }); responseAck.ack(); // no exception - assert.equal( - setTimeoutSpy.firstCall.args[1], - 3001, - 'a 3 second timeout for the unhandledRequestHandler callback is expected', - ); + assert.equal(setTimeoutSpy.firstCall.args[1], 3001, 'a 3 second timeout for the unhandledRequestHandler callback is expected'); }); }); diff --git a/test/unit/receivers/SocketModeFunctions.spec.ts b/test/unit/receivers/SocketModeFunctions.spec.ts index fe68da142..1514f8854 100644 --- a/test/unit/receivers/SocketModeFunctions.spec.ts +++ b/test/unit/receivers/SocketModeFunctions.spec.ts @@ -1,8 +1,9 @@ -import { assert } from 'chai'; +import assert from 'node:assert/strict'; import { AuthorizationError, ReceiverMultipleAckError } from '../../../src/errors'; import { defaultProcessEventErrorHandler } from '../../../src/receivers/SocketModeFunctions'; import type { ReceiverEvent } from '../../../src/types'; import { createFakeLogger } from '../helpers'; +import { describe, it } from 'node:test'; describe('SocketModeFunctions', async () => { describe('Error handlers for event processing', async () => { @@ -19,7 +20,7 @@ describe('SocketModeFunctions', async () => { logger, event, }); - assert.isFalse(shouldBeAcked); + assert.strictEqual(shouldBeAcked, false); }); it('should return true if passed an AuthorizationError', async () => { const event: ReceiverEvent = { @@ -31,7 +32,7 @@ describe('SocketModeFunctions', async () => { logger, event, }); - assert.isTrue(shouldBeAcked); + assert.strictEqual(shouldBeAcked, true); }); }); }); diff --git a/test/unit/receivers/SocketModeReceiver.spec.ts b/test/unit/receivers/SocketModeReceiver.spec.ts index 0c8568c65..56cfafa3d 100644 --- a/test/unit/receivers/SocketModeReceiver.spec.ts +++ b/test/unit/receivers/SocketModeReceiver.spec.ts @@ -3,7 +3,7 @@ import { IncomingMessage, ServerResponse } from 'node:http'; import path from 'node:path'; import { InstallProvider } from '@slack/oauth'; import { SocketModeClient } from '@slack/socket-mode'; -import { assert } from 'chai'; +import assert from 'node:assert/strict'; import type { ParamsDictionary } from 'express-serve-static-core'; import { match } from 'path-to-regexp'; import sinon from 'sinon'; @@ -22,6 +22,7 @@ import { withHttpCreateServer, withHttpsCreateServer, } from '../helpers'; +import { beforeEach, describe, it } from 'node:test'; // Loading the system under test using overrides function importSocketModeReceiver( @@ -66,7 +67,7 @@ describe('SocketModeReceiver', () => { userScopes: ['chat:write'], }, }); - assert.isNotNull(receiver); + assert.notStrictEqual(receiver, null); }); it('should allow for customizing port the socket listens on', async () => { const SocketModeReceiver = importSocketModeReceiver(overrides); @@ -85,7 +86,7 @@ describe('SocketModeReceiver', () => { port: customPort, }, }); - assert.isNotNull(receiver); + assert.notStrictEqual(receiver, null); }); it('should allow for extracting additional values from Socket Mode messages', async () => { const SocketModeReceiver = importSocketModeReceiver(overrides); @@ -95,7 +96,7 @@ describe('SocketModeReceiver', () => { logger: noopLogger, customPropertiesExtractor: ({ type, body }) => ({ payload_type: type, body }), }); - assert.isNotNull(receiver); + assert.notStrictEqual(receiver, null); }); it('should pass clientPingTimeout to SocketModeClient', async () => { const constructorSpy = sinon.spy(); @@ -117,7 +118,7 @@ describe('SocketModeReceiver', () => { logger: noopLogger, clientPingTimeout: 15000, }); - assert.isNotNull(receiver); + assert.notStrictEqual(receiver, null); sinon.assert.calledOnce(constructorSpy); const constructorArgs = constructorSpy.firstCall.args[0]; assert.equal(constructorArgs.clientPingTimeout, 15000); @@ -142,7 +143,7 @@ describe('SocketModeReceiver', () => { logger: noopLogger, serverPingTimeout: 60000, }); - assert.isNotNull(receiver); + assert.notStrictEqual(receiver, null); sinon.assert.calledOnce(constructorSpy); const constructorArgs = constructorSpy.firstCall.args[0]; assert.equal(constructorArgs.serverPingTimeout, 60000); @@ -170,7 +171,7 @@ describe('SocketModeReceiver', () => { pingPongLoggingEnabled: true, autoReconnectEnabled: false, }); - assert.isNotNull(receiver); + assert.notStrictEqual(receiver, null); sinon.assert.calledOnce(constructorSpy); const constructorArgs = constructorSpy.firstCall.args[0]; assert.equal(constructorArgs.clientPingTimeout, 15000); @@ -197,14 +198,14 @@ describe('SocketModeReceiver', () => { appToken: 'my-secret', logger: noopLogger, }); - assert.isNotNull(receiver); + assert.notStrictEqual(receiver, null); sinon.assert.calledOnce(constructorSpy); const constructorArgs = constructorSpy.firstCall.args[0]; assert.equal(constructorArgs.appToken, 'my-secret'); - assert.isUndefined(constructorArgs.clientPingTimeout); - assert.isUndefined(constructorArgs.serverPingTimeout); - assert.isUndefined(constructorArgs.pingPongLoggingEnabled); - assert.isUndefined(constructorArgs.autoReconnectEnabled); + assert.strictEqual(constructorArgs.clientPingTimeout, undefined); + assert.strictEqual(constructorArgs.serverPingTimeout, undefined); + assert.strictEqual(constructorArgs.pingPongLoggingEnabled, undefined); + assert.strictEqual(constructorArgs.autoReconnectEnabled, undefined); }); it('should throw an error if redirect uri options supplied invalid or incomplete', async () => { const SocketModeReceiver = importSocketModeReceiver(overrides); @@ -227,10 +228,9 @@ describe('SocketModeReceiver', () => { redirectUri, installerOptions, }); - assert.isNotNull(receiver); + assert.notStrictEqual(receiver, null); // redirectUri supplied, but no redirectUriPath - assert.throws( - () => + assert.throws(() => new SocketModeReceiver({ appToken, clientId, @@ -238,12 +238,9 @@ describe('SocketModeReceiver', () => { stateSecret, scopes, redirectUri, - }), - AppInitializationError, - ); + }), AppInitializationError); // inconsistent redirectUriPath - assert.throws( - () => + assert.throws(() => new SocketModeReceiver({ appToken, clientId: 'my-clientId', @@ -254,12 +251,9 @@ describe('SocketModeReceiver', () => { installerOptions: { redirectUriPath: '/hiya', }, - }), - AppInitializationError, - ); + }), AppInitializationError); // inconsistent redirectUri - assert.throws( - () => + assert.throws(() => new SocketModeReceiver({ appToken, clientId: 'my-clientId', @@ -268,9 +262,7 @@ describe('SocketModeReceiver', () => { scopes, redirectUri: 'http://example.com/hiya', installerOptions, - }), - AppInitializationError, - ); + }), AppInitializationError); }); }); describe('request handling', () => { @@ -299,7 +291,7 @@ describe('SocketModeReceiver', () => { userScopes, }, }); - assert.isNotNull(receiver); + assert.notStrictEqual(receiver, null); receiver.installer = installProviderStub as unknown as InstallProvider; const fakeReq = sinon.createStubInstance(IncomingMessage); fakeReq.url = '/nope'; @@ -331,7 +323,7 @@ describe('SocketModeReceiver', () => { userScopes, }, }); - assert.isNotNull(receiver); + assert.notStrictEqual(receiver, null); receiver.installer = installProviderStub as unknown as InstallProvider; const fakeReq = { url: '/hiya', @@ -366,7 +358,7 @@ describe('SocketModeReceiver', () => { userScopes, }, }); - assert.isNotNull(receiver); + assert.notStrictEqual(receiver, null); receiver.installer = installProviderStub as unknown as InstallProvider; const fakeReq = { url: '/hiya', @@ -401,7 +393,7 @@ describe('SocketModeReceiver', () => { userScopes, }, }); - assert.isNotNull(receiver); + assert.notStrictEqual(receiver, null); receiver.installer = installProviderStub as unknown as InstallProvider; const fakeReq = { url: '/hiya', @@ -440,7 +432,7 @@ describe('SocketModeReceiver', () => { callbackOptions, }, }); - assert.isNotNull(receiver); + assert.notStrictEqual(receiver, null); receiver.installer = installProviderStub as unknown as InstallProvider; const fakeReq = { url: '/heyo', @@ -490,7 +482,7 @@ describe('SocketModeReceiver', () => { metadata, }, }); - assert.isNotNull(receiver); + assert.notStrictEqual(receiver, null); receiver.installer = installProviderStub as unknown as InstallProvider; const fakeReq = sinon.createStubInstance(IncomingMessage); fakeReq.url = '/heyo'; @@ -518,7 +510,7 @@ describe('SocketModeReceiver', () => { appToken: 'my-secret', customRoutes, }); - assert.isNotNull(receiver); + assert.notStrictEqual(receiver, null); receiver.installer = installProviderStub as unknown as InstallProvider; const fakeReq = sinon.createStubInstance(IncomingMessage); @@ -556,7 +548,7 @@ describe('SocketModeReceiver', () => { appToken: 'my-secret', customRoutes, }); - assert.isNotNull(receiver); + assert.notStrictEqual(receiver, null); receiver.installer = installProviderStub as unknown as InstallProvider; const fakeReq = sinon.createStubInstance(IncomingMessage); @@ -594,7 +586,7 @@ describe('SocketModeReceiver', () => { appToken: 'my-secret', customRoutes, }); - assert.isNotNull(receiver); + assert.notStrictEqual(receiver, null); receiver.installer = installProviderStub as unknown as InstallProvider; const fakeReq = sinon.createStubInstance(IncomingMessage); @@ -635,7 +627,7 @@ describe('SocketModeReceiver', () => { appToken: 'my-secret', customRoutes, }); - assert.isNotNull(receiver); + assert.notStrictEqual(receiver, null); receiver.installer = installProviderStub as unknown as InstallProvider; const fakeReq = sinon.createStubInstance(IncomingMessage); @@ -678,7 +670,7 @@ describe('SocketModeReceiver', () => { appToken: 'my-secret', customRoutes, }); - assert.isNotNull(receiver); + assert.notStrictEqual(receiver, null); receiver.installer = installProviderStub as unknown as InstallProvider; const fakeReq = sinon.createStubInstance(IncomingMessage); @@ -712,10 +704,7 @@ describe('SocketModeReceiver', () => { // biome-ignore lint/suspicious/noExplicitAny: typing as any to intentionally have missing required keys const customRoutes = [{ handler: sinon.fake() }] as any; - assert.throws( - () => new SocketModeReceiver({ appToken: 'my-secret', customRoutes }), - CustomRouteInitializationError, - ); + assert.throws(() => new SocketModeReceiver({ appToken: 'my-secret', customRoutes }), CustomRouteInitializationError); }); }); }); @@ -737,7 +726,7 @@ describe('SocketModeReceiver', () => { userScopes: ['chat:write'], }, }); - assert.isNotNull(receiver); + assert.notStrictEqual(receiver, null); receiver.client = clientStub as unknown as SocketModeClient; await receiver.start(); assert(clientStub.start.called); @@ -761,7 +750,7 @@ describe('SocketModeReceiver', () => { userScopes: ['chat:write'], }, }); - assert.isNotNull(receiver); + assert.notStrictEqual(receiver, null); receiver.client = clientStub as unknown as SocketModeClient; await receiver.stop(); assert(clientStub.disconnect.called); diff --git a/test/unit/receivers/SocketModeResponseAck.spec.ts b/test/unit/receivers/SocketModeResponseAck.spec.ts index a10020d48..6a3743e89 100644 --- a/test/unit/receivers/SocketModeResponseAck.spec.ts +++ b/test/unit/receivers/SocketModeResponseAck.spec.ts @@ -1,9 +1,10 @@ -import { assert } from 'chai'; +import assert from 'node:assert/strict'; import sinon from 'sinon'; import { expectType } from 'tsd'; import { SocketModeResponseAck } from '../../../src/receivers/SocketModeResponseAck'; import type { ResponseAck } from '../../../src/types'; import { createFakeLogger } from '../helpers'; +import { beforeEach, describe, it } from 'node:test'; describe('SocketModeResponseAck', async () => { const fakeSocketModeClientAck = sinon.fake(); @@ -18,8 +19,8 @@ describe('SocketModeResponseAck', async () => { logger: fakeLogger, socketModeClientAck: fakeSocketModeClientAck, }); - assert.isDefined(responseAck); - assert.isDefined(responseAck.bind()); + assert.notStrictEqual(responseAck, undefined); + assert.notStrictEqual(responseAck.bind(), undefined); expectType(responseAck); }); diff --git a/test/unit/receivers/verify-request.spec.ts b/test/unit/receivers/verify-request.spec.ts index 7554abb9c..3153d2089 100644 --- a/test/unit/receivers/verify-request.spec.ts +++ b/test/unit/receivers/verify-request.spec.ts @@ -1,6 +1,7 @@ import { createHmac } from 'node:crypto'; -import { assert } from 'chai'; +import assert from 'node:assert/strict'; import { isValidSlackRequest, verifySlackRequest } from '../../../src/receivers/verify-request'; +import { describe, it } from 'node:test'; describe('Request verification', async () => { const signingSecret = 'secret'; @@ -37,11 +38,9 @@ describe('Request verification', async () => { body: rawBody, }); } catch (e) { - assert.propertyVal( - e, - 'message', - 'Failed to verify authenticity: x-slack-request-timestamp must differ from system time by no more than 5 minutes or request is stale', - ); + assert.ok(e && typeof e === 'object'); + assert.ok('message' in e); + assert.deepStrictEqual((e as unknown as Record)['message'], 'Failed to verify authenticity: x-slack-request-timestamp must differ from system time by no more than 5 minutes or request is stale'); } }); it('should detect an invalid signature', async () => { @@ -57,7 +56,9 @@ describe('Request verification', async () => { body: rawBody, }); } catch (e) { - assert.propertyVal(e, 'message', 'Failed to verify authenticity: signature mismatch'); + assert.ok(e && typeof e === 'object'); + assert.ok('message' in e); + assert.deepStrictEqual((e as unknown as Record)['message'], 'Failed to verify authenticity: signature mismatch'); } }); }); @@ -69,16 +70,14 @@ describe('Request verification', async () => { const hmac = createHmac('sha256', signingSecret); hmac.update(`v0:${timestamp}:${rawBody}`); const signature = hmac.digest('hex'); - assert.isTrue( - isValidSlackRequest({ + assert.strictEqual(isValidSlackRequest({ signingSecret, headers: { 'x-slack-signature': `v0=${signature}`, 'x-slack-request-timestamp': timestamp, }, body: rawBody, - }), - ); + }), true); }); it('should detect an invalid timestamp', async () => { const timestamp = Math.floor(Date.now() / 1000) - 600; // 10 minutes @@ -86,30 +85,26 @@ describe('Request verification', async () => { const hmac = createHmac('sha256', signingSecret); hmac.update(`v0:${timestamp}:${rawBody}`); const signature = hmac.digest('hex'); - assert.isFalse( - isValidSlackRequest({ + assert.strictEqual(isValidSlackRequest({ signingSecret, headers: { 'x-slack-signature': `v0=${signature}`, 'x-slack-request-timestamp': timestamp, }, body: rawBody, - }), - ); + }), false); }); it('should detect an invalid signature', async () => { const timestamp = Math.floor(Date.now() / 1000); const rawBody = '{"foo":"bar"}'; - assert.isFalse( - isValidSlackRequest({ + assert.strictEqual(isValidSlackRequest({ signingSecret, headers: { 'x-slack-signature': 'v0=invalid-signature', 'x-slack-request-timestamp': timestamp, }, body: rawBody, - }), - ); + }), false); }); }); }); diff --git a/tsconfig.test.json b/tsconfig.test.json new file mode 100644 index 000000000..8315678f6 --- /dev/null +++ b/tsconfig.test.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": true, + "types": ["node"] + }, + "include": [ + "src/**/*", + "test/**/*" + ], + "exclude": [] +}