From f53fde5fe189882778afc8f331ddbf33c93cabe6 Mon Sep 17 00:00:00 2001 From: "@theorderingmachine" Date: Wed, 8 Apr 2026 13:24:27 -0700 Subject: [PATCH 1/4] chore: remove mocha chai and c8 --- .github/maintainers_guide.md | 4 +- .vscode/launch.json | 17 +- AGENTS.md | 8 +- package.json | 9 +- test/unit/App/basic.spec.ts | 2 +- test/unit/App/middlewares/arguments.spec.ts | 2 +- test/unit/App/middlewares/global.spec.ts | 2 +- test/unit/App/middlewares/listener.spec.ts | 2 +- test/unit/App/routing-action.spec.ts | 2 +- test/unit/App/routing-function.spec.ts | 2 +- test/unit/Assistant.spec.ts | 2 +- test/unit/AssistantThreadContextStore.spec.ts | 2 +- test/unit/CustomFunction.spec.ts | 2 +- test/unit/WorkflowStep.spec.ts | 2 +- .../context/create-function-complete.spec.ts | 2 +- .../unit/context/create-function-fail.spec.ts | 2 +- test/unit/context/create-respond.spec.ts | 2 +- test/unit/context/create-say-stream.spec.ts | 2 +- test/unit/context/create-say.spec.ts | 2 +- test/unit/context/create-set-status.spec.ts | 2 +- test/unit/conversation-store.spec.ts | 6 +- test/unit/errors.spec.ts | 2 +- test/unit/helpers.spec.ts | 2 +- test/unit/helpers/assert.ts | 163 ++++++++++++++++++ test/unit/middleware/builtin.spec.ts | 6 +- test/unit/node-test-globals.cjs | 8 + test/unit/node-test-globals.d.ts | 19 ++ test/unit/receivers/AwsLambdaReceiver.spec.ts | 2 +- test/unit/receivers/ExpressReceiver.spec.ts | 2 +- .../receivers/HTTPModuleFunctions.spec.ts | 2 +- test/unit/receivers/HTTPReceiver.spec.ts | 2 +- test/unit/receivers/HTTPResponseAck.spec.ts | 18 +- .../receivers/SocketModeFunctions.spec.ts | 2 +- .../unit/receivers/SocketModeReceiver.spec.ts | 2 +- .../receivers/SocketModeResponseAck.spec.ts | 2 +- test/unit/receivers/verify-request.spec.ts | 2 +- tsconfig.test.json | 13 ++ 37 files changed, 263 insertions(+), 60 deletions(-) create mode 100644 test/unit/helpers/assert.ts create mode 100644 test/unit/node-test-globals.cjs create mode 100644 test/unit/node-test-globals.d.ts create mode 100644 tsconfig.test.json diff --git a/.github/maintainers_guide.md b/.github/maintainers_guide.md index 2edf3821e..98a26df4f 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 --require ts-node/register --require source-map-support/register --require ./test/unit/node-test-globals.cjs --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..66c32f1f1 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -8,14 +8,23 @@ "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", + "--require", + "ts-node/register", + "--require", + "source-map-support/register", + "--require", + "./test/unit/node-test-globals.cjs", + "--test", + "test/unit/**/*.spec.ts" + ], "cwd": "${workspaceFolder}", - "runtimeExecutable": null, "env": { "NODE_ENV": "testing", - "TS_NODE_PROJECT": "tsconfig.test.json" + "TS_NODE_PROJECT": "tsconfig.json" }, "skipFiles": ["/**"] } diff --git a/AGENTS.md b/AGENTS.md index 697b4af49..4f60fc063 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** use the local `test/unit/helpers/assert.ts` compatibility helper backed by Node.js `assert` and Sinon assertions - **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 `test/unit/node-test-globals.cjs` - **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..808f2557d 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": "TS_NODE_PROJECT=tsconfig.test.json TS_NODE_FILES=true node --require ts-node/register --require source-map-support/register --require ./test/unit/node-test-globals.cjs --test test/unit/**/*.spec.ts", + "test:coverage": "TS_NODE_PROJECT=tsconfig.test.json TS_NODE_FILES=true node --experimental-test-coverage --require ts-node/register --require source-map-support/register --require ./test/unit/node-test-globals.cjs --test test/unit/**/*.spec.ts", "test:types": "tsd --files test/types", "watch": "npx nodemon --watch 'src' --ext 'ts' --exec npm run build" }, @@ -61,15 +61,10 @@ "@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", diff --git a/test/unit/App/basic.spec.ts b/test/unit/App/basic.spec.ts index b7a8ad78c..93e01a765 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 '../helpers/assert'; import sinon from 'sinon'; import { ErrorCode } from '../../../src/errors'; import SocketModeReceiver from '../../../src/receivers/SocketModeReceiver'; diff --git a/test/unit/App/middlewares/arguments.spec.ts b/test/unit/App/middlewares/arguments.spec.ts index d28b16d65..e3c40128c 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 '../../helpers/assert'; import sinon, { type SinonSpy } from 'sinon'; import { LogLevel } from '../../../../src/App'; import type { SayStreamFn } from '../../../../src/context/create-say-stream'; diff --git a/test/unit/App/middlewares/global.spec.ts b/test/unit/App/middlewares/global.spec.ts index 1f332e971..1ff317122 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 '../../helpers/assert'; import sinon, { type SinonSpy } from 'sinon'; import type App from '../../../../src/App'; import type { ExtendedErrorHandlerArgs } from '../../../../src/App'; diff --git a/test/unit/App/middlewares/listener.spec.ts b/test/unit/App/middlewares/listener.spec.ts index f6c4815ba..36d8afb12 100644 --- a/test/unit/App/middlewares/listener.spec.ts +++ b/test/unit/App/middlewares/listener.spec.ts @@ -1,4 +1,4 @@ -import { assert } from 'chai'; +import { assert } from '../../helpers/assert'; import sinon, { type SinonSpy } from 'sinon'; import type App from '../../../../src/App'; import { ErrorCode, isCodedError } from '../../../../src/errors'; diff --git a/test/unit/App/routing-action.spec.ts b/test/unit/App/routing-action.spec.ts index 5415e7468..fa247a39f 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 '../helpers/assert'; import sinon, { type SinonSpy } from 'sinon'; import type App from '../../../src/App'; import { diff --git a/test/unit/App/routing-function.spec.ts b/test/unit/App/routing-function.spec.ts index 90db27f40..db50d24d9 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 '../helpers/assert'; import sinon from 'sinon'; import type App from '../../../src/App'; import { diff --git a/test/unit/Assistant.spec.ts b/test/unit/Assistant.spec.ts index 6818bae5c..1712eaf27 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 './helpers/assert'; import sinon from 'sinon'; import { type AllAssistantMiddlewareArgs, diff --git a/test/unit/AssistantThreadContextStore.spec.ts b/test/unit/AssistantThreadContextStore.spec.ts index d1453c332..faafb6533 100644 --- a/test/unit/AssistantThreadContextStore.spec.ts +++ b/test/unit/AssistantThreadContextStore.spec.ts @@ -1,5 +1,5 @@ import type { WebClient } from '@slack/web-api'; -import { assert } from 'chai'; +import { assert } from './helpers/assert'; import sinon from 'sinon'; import { extractThreadInfo } from '../../src/Assistant'; import { DefaultThreadContextStore } from '../../src/AssistantThreadContextStore'; diff --git a/test/unit/CustomFunction.spec.ts b/test/unit/CustomFunction.spec.ts index 5aa123579..ec10330a7 100644 --- a/test/unit/CustomFunction.spec.ts +++ b/test/unit/CustomFunction.spec.ts @@ -1,4 +1,4 @@ -import { assert } from 'chai'; +import { assert } from './helpers/assert'; import { CustomFunction, type SlackCustomFunctionMiddlewareArgs, diff --git a/test/unit/WorkflowStep.spec.ts b/test/unit/WorkflowStep.spec.ts index dab9cc663..7dd99191a 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 './helpers/assert'; import sinon from 'sinon'; import { type AllWorkflowStepMiddlewareArgs, diff --git a/test/unit/context/create-function-complete.spec.ts b/test/unit/context/create-function-complete.spec.ts index c96b6503f..8470ee925 100644 --- a/test/unit/context/create-function-complete.spec.ts +++ b/test/unit/context/create-function-complete.spec.ts @@ -1,5 +1,5 @@ import { WebClient } from '@slack/web-api'; -import { assert } from 'chai'; +import { assert } from '../helpers/assert'; import sinon from 'sinon'; import { createFunctionComplete } from '../../../src/context'; diff --git a/test/unit/context/create-function-fail.spec.ts b/test/unit/context/create-function-fail.spec.ts index 41c04ec71..9e1bca8df 100644 --- a/test/unit/context/create-function-fail.spec.ts +++ b/test/unit/context/create-function-fail.spec.ts @@ -1,5 +1,5 @@ import { WebClient } from '@slack/web-api'; -import { assert } from 'chai'; +import { assert } from '../helpers/assert'; import sinon from 'sinon'; import { createFunctionFail } from '../../../src/context'; diff --git a/test/unit/context/create-respond.spec.ts b/test/unit/context/create-respond.spec.ts index a24a37e57..5b2363dc8 100644 --- a/test/unit/context/create-respond.spec.ts +++ b/test/unit/context/create-respond.spec.ts @@ -1,5 +1,5 @@ import type { AxiosInstance } from 'axios'; -import { assert } from 'chai'; +import { assert } from '../helpers/assert'; import sinon from 'sinon'; import { createRespond } from '../../../src/context'; diff --git a/test/unit/context/create-say-stream.spec.ts b/test/unit/context/create-say-stream.spec.ts index b7a9010c6..3a9607c3b 100644 --- a/test/unit/context/create-say-stream.spec.ts +++ b/test/unit/context/create-say-stream.spec.ts @@ -1,5 +1,5 @@ import { WebClient } from '@slack/web-api'; -import { assert } from 'chai'; +import { assert } from '../helpers/assert'; import sinon from 'sinon'; import { createSayStream } from '../../../src/context'; import type { Context } from '../../../src/types'; diff --git a/test/unit/context/create-say.spec.ts b/test/unit/context/create-say.spec.ts index 006e23b40..8ee651a34 100644 --- a/test/unit/context/create-say.spec.ts +++ b/test/unit/context/create-say.spec.ts @@ -1,5 +1,5 @@ import { WebClient } from '@slack/web-api'; -import { assert } from 'chai'; +import { assert } from '../helpers/assert'; import sinon from 'sinon'; import { createSay } from '../../../src/context'; diff --git a/test/unit/context/create-set-status.spec.ts b/test/unit/context/create-set-status.spec.ts index 4709e19d0..27f5a5d1d 100644 --- a/test/unit/context/create-set-status.spec.ts +++ b/test/unit/context/create-set-status.spec.ts @@ -1,5 +1,5 @@ import { WebClient } from '@slack/web-api'; -import { assert } from 'chai'; +import { assert } from '../helpers/assert'; import sinon from 'sinon'; import { createSetStatus } from '../../../src/context'; diff --git a/test/unit/conversation-store.spec.ts b/test/unit/conversation-store.spec.ts index e677c0ed5..d904601f4 100644 --- a/test/unit/conversation-store.spec.ts +++ b/test/unit/conversation-store.spec.ts @@ -1,7 +1,7 @@ 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 './helpers/assert'; import sinon, { type SinonSpy } from 'sinon'; import type { AnyMiddlewareArgs, Context, NextFn } from '../../src/types'; import { type Override, createFakeLogger, delay, proxyquire } from './helpers'; @@ -137,7 +137,7 @@ 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. + // NOTE: node:assert types do not offer assertion signatures here. if (dummyContext.updateConversation === undefined) { assert.fail(); } @@ -173,7 +173,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(); } diff --git a/test/unit/errors.spec.ts b/test/unit/errors.spec.ts index ec907eaa2..e201d0d1e 100644 --- a/test/unit/errors.spec.ts +++ b/test/unit/errors.spec.ts @@ -1,4 +1,4 @@ -import { assert } from 'chai'; +import { assert } from './helpers/assert'; import { AppInitializationError, AuthorizationError, diff --git a/test/unit/helpers.spec.ts b/test/unit/helpers.spec.ts index 688d23069..816d47318 100644 --- a/test/unit/helpers.spec.ts +++ b/test/unit/helpers.spec.ts @@ -1,4 +1,4 @@ -import { assert } from 'chai'; +import { assert } from './helpers/assert'; import { IncomingEventType, extractEventChannelId, diff --git a/test/unit/helpers/assert.ts b/test/unit/helpers/assert.ts new file mode 100644 index 000000000..c4448e7a8 --- /dev/null +++ b/test/unit/helpers/assert.ts @@ -0,0 +1,163 @@ +import { AssertionError } from 'node:assert'; +import nodeAssert from 'node:assert/strict'; +import sinon from 'sinon'; + +type AssertFn = ((value: unknown, message?: string) => asserts value) & { + ok: typeof nodeAssert.ok; + fail: typeof nodeAssert.fail; + equal: typeof nodeAssert.equal; + notEqual: typeof nodeAssert.notEqual; + strictEqual: typeof nodeAssert.strictEqual; + deepEqual: typeof nodeAssert.deepEqual; + deepStrictEqual: typeof nodeAssert.deepStrictEqual; + throws: typeof nodeAssert.throws; + exists(value: unknown, message?: string): void; + notExists(value: unknown, message?: string): void; + isDefined(value: unknown, message?: string): void; + isUndefined(value: unknown, message?: string): void; + isTrue(value: unknown, message?: string): void; + isFalse(value: unknown, message?: string): void; + isOk(value: unknown, message?: string): void; + isNotNull(value: unknown, message?: string): void; + isArray(value: unknown, message?: string): void; + isFunction(value: unknown, message?: string): void; + isAtLeast(value: number, min: number, message?: string): void; + isEmpty(value: unknown, message?: string): void; + lengthOf(value: { length: number }, expected: number, message?: string): void; + typeOf(value: unknown, expected: string, message?: string): void; + instanceOf(value: unknown, ctor: new (...args: any[]) => any, message?: string): void; + notInstanceOf(value: unknown, ctor: new (...args: any[]) => any, message?: string): void; + property(object: object, prop: PropertyKey, message?: string): void; + notProperty(object: object, prop: PropertyKey, message?: string): void; + propertyVal(object: unknown, prop: PropertyKey, expected: unknown, message?: string): void; + sameMembers(actual: unknown[], expected: unknown[], message?: string): void; + called(spy: sinon.SinonSpy, message?: string): void; + notCalled(spy: sinon.SinonSpy, message?: string): void; + calledOnce(spy: sinon.SinonSpy, message?: string): void; + calledTwice(spy: sinon.SinonSpy, message?: string): void; + calledThrice(spy: sinon.SinonSpy, message?: string): void; + callCount(spy: sinon.SinonSpy, count: number, message?: string): void; + calledWith(spy: sinon.SinonSpy, ...args: unknown[]): void; + calledWithMatch(spy: sinon.SinonSpy, ...args: unknown[]): void; + calledOnceWithExactly(spy: sinon.SinonSpy, ...args: unknown[]): void; + neverCalledWith(spy: sinon.SinonSpy, ...args: unknown[]): void; +}; + +const assert: AssertFn = Object.assign( + (value: unknown, message?: string): asserts value => nodeAssert.ok(value, message), + { + ok: nodeAssert.ok, + fail: nodeAssert.fail, + equal: nodeAssert.equal, + notEqual: nodeAssert.notEqual, + strictEqual: nodeAssert.strictEqual, + deepEqual: nodeAssert.deepEqual, + deepStrictEqual: nodeAssert.deepStrictEqual, + throws: nodeAssert.throws, + + exists(value: unknown, message?: string): void { + nodeAssert.notEqual(value, null, message); + }, + notExists(value: unknown, message?: string): void { + nodeAssert.equal(value, null, message); + }, + isDefined(value: unknown, message?: string): void { + nodeAssert.notStrictEqual(value, undefined, message); + }, + isUndefined(value: unknown, message?: string): void { + nodeAssert.strictEqual(value, undefined, message); + }, + isTrue(value: unknown, message?: string): void { + nodeAssert.strictEqual(value, true, message); + }, + isFalse(value: unknown, message?: string): void { + nodeAssert.strictEqual(value, false, message); + }, + isOk(value: unknown, message?: string): void { + nodeAssert.ok(value, message); + }, + isNotNull(value: unknown, message?: string): void { + nodeAssert.notStrictEqual(value, null, message); + }, + isArray(value: unknown, message?: string): void { + nodeAssert.ok(Array.isArray(value), message ?? 'expected value to be an array'); + }, + isFunction(value: unknown, message?: string): void { + nodeAssert.strictEqual(typeof value, 'function', message); + }, + isAtLeast(value: number, min: number, message?: string): void { + nodeAssert.ok(value >= min, message ?? `expected ${value} to be at least ${min}`); + }, + isEmpty(value: unknown, message?: string): void { + if (Array.isArray(value) || typeof value === 'string') { + nodeAssert.strictEqual(value.length, 0, message); + return; + } + if (value && typeof value === 'object') { + nodeAssert.strictEqual(Object.keys(value).length, 0, message); + return; + } + nodeAssert.fail(message ?? 'expected value to be empty'); + }, + lengthOf(value: { length: number }, expected: number, message?: string): void { + nodeAssert.strictEqual(value.length, expected, message); + }, + typeOf(value: unknown, expected: string, message?: string): void { + nodeAssert.strictEqual(typeof value, expected, message); + }, + instanceOf(value: unknown, ctor: new (...args: any[]) => any, message?: string): void { + nodeAssert.ok(value instanceof ctor, message); + }, + notInstanceOf(value: unknown, ctor: new (...args: any[]) => any, message?: string): void { + nodeAssert.ok(!(value instanceof ctor), message); + }, + property(object: object, prop: PropertyKey, message?: string): void { + nodeAssert.ok(prop in object, message); + }, + notProperty(object: object, prop: PropertyKey, message?: string): void { + nodeAssert.ok(!(prop in object), message); + }, + propertyVal(object: unknown, prop: PropertyKey, expected: unknown, message?: string): void { + nodeAssert.ok(object && typeof object === 'object', message ?? 'expected value to be an object'); + nodeAssert.ok(prop in object, message); + nodeAssert.deepStrictEqual((object as Record)[prop], expected, message); + }, + sameMembers(actual: unknown[], expected: unknown[], message?: string): void { + const normalize = (arr: unknown[]) => [...arr].sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b))); + nodeAssert.deepStrictEqual(normalize(actual), normalize(expected), message); + }, + + called(spy: sinon.SinonSpy): void { + sinon.assert.called(spy); + }, + notCalled(spy: sinon.SinonSpy): void { + sinon.assert.notCalled(spy); + }, + calledOnce(spy: sinon.SinonSpy): void { + sinon.assert.calledOnce(spy); + }, + calledTwice(spy: sinon.SinonSpy): void { + sinon.assert.calledTwice(spy); + }, + calledThrice(spy: sinon.SinonSpy): void { + sinon.assert.calledThrice(spy); + }, + callCount(spy: sinon.SinonSpy, count: number): void { + sinon.assert.callCount(spy, count); + }, + calledWith(spy: sinon.SinonSpy, ...args: unknown[]): void { + sinon.assert.calledWith(spy, ...args); + }, + calledWithMatch(spy: sinon.SinonSpy, ...args: unknown[]): void { + sinon.assert.calledWithMatch(spy, ...args); + }, + calledOnceWithExactly(spy: sinon.SinonSpy, ...args: unknown[]): void { + sinon.assert.calledOnceWithExactly(spy, ...args); + }, + neverCalledWith(spy: sinon.SinonSpy, ...args: unknown[]): void { + sinon.assert.neverCalledWith(spy, ...args); + }, + }, +) as AssertFn; + +export { assert, AssertionError }; diff --git a/test/unit/middleware/builtin.spec.ts b/test/unit/middleware/builtin.spec.ts index 9d9422531..71f95db8b 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 '../helpers/assert'; import sinon from 'sinon'; import { expectType } from 'tsd'; import { ErrorCode } from '../../../src/errors'; @@ -40,7 +40,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); @@ -63,7 +63,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); diff --git a/test/unit/node-test-globals.cjs b/test/unit/node-test-globals.cjs new file mode 100644 index 000000000..cefa5c0c4 --- /dev/null +++ b/test/unit/node-test-globals.cjs @@ -0,0 +1,8 @@ +const test = require('node:test'); + +global.describe = test.describe; +global.it = test.it; +global.before = test.before; +global.after = test.after; +global.beforeEach = test.beforeEach; +global.afterEach = test.afterEach; diff --git a/test/unit/node-test-globals.d.ts b/test/unit/node-test-globals.d.ts new file mode 100644 index 000000000..0627c3c6e --- /dev/null +++ b/test/unit/node-test-globals.d.ts @@ -0,0 +1,19 @@ +import type { + after as nodeAfter, + afterEach as nodeAfterEach, + before as nodeBefore, + beforeEach as nodeBeforeEach, + describe as nodeDescribe, + it as nodeIt, +} from 'node:test'; + +declare global { + const describe: typeof nodeDescribe; + const it: typeof nodeIt; + const before: typeof nodeBefore; + const after: typeof nodeAfter; + const beforeEach: typeof nodeBeforeEach; + const afterEach: typeof nodeAfterEach; +} + +export {}; diff --git a/test/unit/receivers/AwsLambdaReceiver.spec.ts b/test/unit/receivers/AwsLambdaReceiver.spec.ts index ba14c3fc6..55466f1aa 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 '../helpers/assert'; import sinon from 'sinon'; import AwsLambdaReceiver from '../../../src/receivers/AwsLambdaReceiver'; import { diff --git a/test/unit/receivers/ExpressReceiver.spec.ts b/test/unit/receivers/ExpressReceiver.spec.ts index f5ec2e41f..3bf32ce0b 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 '../helpers/assert'; import type { Application, IRouter, Request, Response } from 'express'; import sinon, { type SinonFakeTimers } from 'sinon'; import App from '../../../src/App'; diff --git a/test/unit/receivers/HTTPModuleFunctions.spec.ts b/test/unit/receivers/HTTPModuleFunctions.spec.ts index 7db177f25..5c2937849 100644 --- a/test/unit/receivers/HTTPModuleFunctions.spec.ts +++ b/test/unit/receivers/HTTPModuleFunctions.spec.ts @@ -1,6 +1,6 @@ import { createHmac } from 'node:crypto'; import { IncomingMessage, ServerResponse } from 'node:http'; -import { assert } from 'chai'; +import { assert } from '../helpers/assert'; import sinon from 'sinon'; import { AuthorizationError, HTTPReceiverDeferredRequestError, ReceiverMultipleAckError } from '../../../src/errors'; diff --git a/test/unit/receivers/HTTPReceiver.spec.ts b/test/unit/receivers/HTTPReceiver.spec.ts index 3d9ab1b43..444352915 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 '../helpers/assert'; import type { ParamsDictionary } from 'express-serve-static-core'; import { match } from 'path-to-regexp'; import sinon from 'sinon'; diff --git a/test/unit/receivers/HTTPResponseAck.spec.ts b/test/unit/receivers/HTTPResponseAck.spec.ts index 5a2cd3de6..92913e1cb 100644 --- a/test/unit/receivers/HTTPResponseAck.spec.ts +++ b/test/unit/receivers/HTTPResponseAck.spec.ts @@ -1,5 +1,5 @@ import { IncomingMessage, ServerResponse } from 'node:http'; -import { assert } from 'chai'; +import { assert } from '../helpers/assert'; import sinon from 'sinon'; import { expectType } from 'tsd'; import { ReceiverMultipleAckError } from '../../../src/errors'; @@ -52,7 +52,7 @@ describe('HTTPResponseAck', async () => { '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; @@ -70,12 +70,10 @@ describe('HTTPResponseAck', async () => { unhandledRequestTimeoutMillis, `a ${unhandledRequestTimeoutMillis} timeout for the unhandledRequestHandler callback is expected`, ); - setTimeout(() => { - assert(spy.calledOnce); - done(); - }, 2); + 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 +86,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; diff --git a/test/unit/receivers/SocketModeFunctions.spec.ts b/test/unit/receivers/SocketModeFunctions.spec.ts index fe68da142..fa99701dd 100644 --- a/test/unit/receivers/SocketModeFunctions.spec.ts +++ b/test/unit/receivers/SocketModeFunctions.spec.ts @@ -1,4 +1,4 @@ -import { assert } from 'chai'; +import { assert } from '../helpers/assert'; import { AuthorizationError, ReceiverMultipleAckError } from '../../../src/errors'; import { defaultProcessEventErrorHandler } from '../../../src/receivers/SocketModeFunctions'; import type { ReceiverEvent } from '../../../src/types'; diff --git a/test/unit/receivers/SocketModeReceiver.spec.ts b/test/unit/receivers/SocketModeReceiver.spec.ts index 0c8568c65..f1d836c54 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 '../helpers/assert'; import type { ParamsDictionary } from 'express-serve-static-core'; import { match } from 'path-to-regexp'; import sinon from 'sinon'; diff --git a/test/unit/receivers/SocketModeResponseAck.spec.ts b/test/unit/receivers/SocketModeResponseAck.spec.ts index a10020d48..4c04b9ed5 100644 --- a/test/unit/receivers/SocketModeResponseAck.spec.ts +++ b/test/unit/receivers/SocketModeResponseAck.spec.ts @@ -1,4 +1,4 @@ -import { assert } from 'chai'; +import { assert } from '../helpers/assert'; import sinon from 'sinon'; import { expectType } from 'tsd'; import { SocketModeResponseAck } from '../../../src/receivers/SocketModeResponseAck'; diff --git a/test/unit/receivers/verify-request.spec.ts b/test/unit/receivers/verify-request.spec.ts index 7554abb9c..449067e61 100644 --- a/test/unit/receivers/verify-request.spec.ts +++ b/test/unit/receivers/verify-request.spec.ts @@ -1,5 +1,5 @@ import { createHmac } from 'node:crypto'; -import { assert } from 'chai'; +import { assert } from '../helpers/assert'; import { isValidSlackRequest, verifySlackRequest } from '../../../src/receivers/verify-request'; describe('Request verification', async () => { 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": [] +} From f16a215d642a2d98cf2d1728842465fd4edf7728 Mon Sep 17 00:00:00 2001 From: "@theorderingmachine" Date: Wed, 8 Apr 2026 13:37:02 -0700 Subject: [PATCH 2/4] test: import unit specs from node:test --- .github/maintainers_guide.md | 2 +- .vscode/launch.json | 5 ++--- AGENTS.md | 2 +- package.json | 4 ++-- test/unit/App/basic.spec.ts | 1 + test/unit/App/middlewares/arguments.spec.ts | 1 + test/unit/App/middlewares/global.spec.ts | 1 + test/unit/App/middlewares/ignore-self.spec.ts | 1 + test/unit/App/middlewares/listener.spec.ts | 1 + test/unit/App/routing-action.spec.ts | 1 + test/unit/App/routing-assistant.spec.ts | 1 + test/unit/App/routing-command.spec.ts | 1 + test/unit/App/routing-event.spec.ts | 1 + test/unit/App/routing-function.spec.ts | 1 + test/unit/App/routing-message.spec.ts | 1 + test/unit/App/routing-options.spec.ts | 1 + test/unit/App/routing-shortcut.spec.ts | 1 + test/unit/App/routing-view.spec.ts | 1 + test/unit/Assistant.spec.ts | 1 + test/unit/AssistantThreadContextStore.spec.ts | 1 + test/unit/CustomFunction.spec.ts | 1 + test/unit/WorkflowStep.spec.ts | 1 + .../context/create-function-complete.spec.ts | 1 + .../unit/context/create-function-fail.spec.ts | 1 + test/unit/context/create-respond.spec.ts | 1 + test/unit/context/create-say-stream.spec.ts | 1 + test/unit/context/create-say.spec.ts | 1 + test/unit/context/create-set-status.spec.ts | 1 + test/unit/conversation-store.spec.ts | 1 + test/unit/errors.spec.ts | 1 + test/unit/helpers.spec.ts | 1 + test/unit/middleware/builtin.spec.ts | 1 + test/unit/node-test-globals.cjs | 8 -------- test/unit/node-test-globals.d.ts | 19 ------------------- test/unit/receivers/AwsLambdaReceiver.spec.ts | 1 + test/unit/receivers/ExpressReceiver.spec.ts | 1 + .../receivers/HTTPModuleFunctions.spec.ts | 1 + test/unit/receivers/HTTPReceiver.spec.ts | 1 + test/unit/receivers/HTTPResponseAck.spec.ts | 1 + .../receivers/SocketModeFunctions.spec.ts | 1 + .../unit/receivers/SocketModeReceiver.spec.ts | 1 + .../receivers/SocketModeResponseAck.spec.ts | 1 + test/unit/receivers/verify-request.spec.ts | 1 + 43 files changed, 43 insertions(+), 34 deletions(-) delete mode 100644 test/unit/node-test-globals.cjs delete mode 100644 test/unit/node-test-globals.d.ts diff --git a/.github/maintainers_guide.md b/.github/maintainers_guide.md index 98a26df4f..dcf7429a5 100644 --- a/.github/maintainers_guide.md +++ b/.github/maintainers_guide.md @@ -21,7 +21,7 @@ Test code should be written in syntax that runs on the oldest supported Node.js #### 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 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 --require ts-node/register --require source-map-support/register --require ./test/unit/node-test-globals.cjs --test test/unit/{test-name}.spec.ts` (replace `{test-name}` with an actual test file path). +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 --require ts-node/register --require source-map-support/register --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 66c32f1f1..7fa7d5135 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -16,15 +16,14 @@ "ts-node/register", "--require", "source-map-support/register", - "--require", - "./test/unit/node-test-globals.cjs", "--test", "test/unit/**/*.spec.ts" ], "cwd": "${workspaceFolder}", "env": { "NODE_ENV": "testing", - "TS_NODE_PROJECT": "tsconfig.json" + "TS_NODE_FILES": "true", + "TS_NODE_PROJECT": "tsconfig.test.json" }, "skipFiles": ["/**"] } diff --git a/AGENTS.md b/AGENTS.md index 4f60fc063..d772f5bbf 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -173,7 +173,7 @@ test/types/ # tsd type tests - **Test files** use `*.spec.ts` suffix - **Assertions** use the local `test/unit/helpers/assert.ts` compatibility helper backed by Node.js `assert` and Sinon assertions - **Mocking** uses sinon (`stub`, `spy`, `fake`) and proxyquire for module-level dependency replacement -- **Test config** lives in `package.json` scripts plus `test/unit/node-test-globals.cjs` +- **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 808f2557d..122ee265d 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.test.json TS_NODE_FILES=true node --require ts-node/register --require source-map-support/register --require ./test/unit/node-test-globals.cjs --test test/unit/**/*.spec.ts", - "test:coverage": "TS_NODE_PROJECT=tsconfig.test.json TS_NODE_FILES=true node --experimental-test-coverage --require ts-node/register --require source-map-support/register --require ./test/unit/node-test-globals.cjs --test test/unit/**/*.spec.ts", + "test:unit": "TS_NODE_PROJECT=tsconfig.test.json TS_NODE_FILES=true node --require ts-node/register --require source-map-support/register --test test/unit/**/*.spec.ts", + "test:coverage": "TS_NODE_PROJECT=tsconfig.test.json TS_NODE_FILES=true node --experimental-test-coverage --require ts-node/register --require source-map-support/register --test test/unit/**/*.spec.ts", "test:types": "tsd --files test/types", "watch": "npx nodemon --watch 'src' --ext 'ts' --exec npm run build" }, diff --git a/test/unit/App/basic.spec.ts b/test/unit/App/basic.spec.ts index 93e01a765..ab52399b6 100644 --- a/test/unit/App/basic.spec.ts +++ b/test/unit/App/basic.spec.ts @@ -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'; diff --git a/test/unit/App/middlewares/arguments.spec.ts b/test/unit/App/middlewares/arguments.spec.ts index e3c40128c..d9b955659 100644 --- a/test/unit/App/middlewares/arguments.spec.ts +++ b/test/unit/App/middlewares/arguments.spec.ts @@ -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; diff --git a/test/unit/App/middlewares/global.spec.ts b/test/unit/App/middlewares/global.spec.ts index 1ff317122..c9eccc2c4 100644 --- a/test/unit/App/middlewares/global.spec.ts +++ b/test/unit/App/middlewares/global.spec.ts @@ -19,6 +19,7 @@ import { withNoopAppMetadata, withNoopWebClient, } from '../../helpers'; +import { beforeEach, describe, it } from 'node:test'; describe('App global middleware Processing', () => { let fakeReceiver: FakeReceiver; 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 36d8afb12..bd0724728 100644 --- a/test/unit/App/middlewares/listener.spec.ts +++ b/test/unit/App/middlewares/listener.spec.ts @@ -3,6 +3,7 @@ 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; diff --git a/test/unit/App/routing-action.spec.ts b/test/unit/App/routing-action.spec.ts index fa247a39f..7bdba3005 100644 --- a/test/unit/App/routing-action.spec.ts +++ b/test/unit/App/routing-action.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/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 db50d24d9..cfa49fdd4 100644 --- a/test/unit/App/routing-function.spec.ts +++ b/test/unit/App/routing-function.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-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 1712eaf27..76438b0b3 100644 --- a/test/unit/Assistant.spec.ts +++ b/test/unit/Assistant.spec.ts @@ -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'); diff --git a/test/unit/AssistantThreadContextStore.spec.ts b/test/unit/AssistantThreadContextStore.spec.ts index faafb6533..f23c4db53 100644 --- a/test/unit/AssistantThreadContextStore.spec.ts +++ b/test/unit/AssistantThreadContextStore.spec.ts @@ -4,6 +4,7 @@ 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', () => { diff --git a/test/unit/CustomFunction.spec.ts b/test/unit/CustomFunction.spec.ts index ec10330a7..e9ffd93ce 100644 --- a/test/unit/CustomFunction.spec.ts +++ b/test/unit/CustomFunction.spec.ts @@ -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 () => {}; diff --git a/test/unit/WorkflowStep.spec.ts b/test/unit/WorkflowStep.spec.ts index 7dd99191a..9b71ad81d 100644 --- a/test/unit/WorkflowStep.spec.ts +++ b/test/unit/WorkflowStep.spec.ts @@ -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'); diff --git a/test/unit/context/create-function-complete.spec.ts b/test/unit/context/create-function-complete.spec.ts index 8470ee925..70cd6b1e0 100644 --- a/test/unit/context/create-function-complete.spec.ts +++ b/test/unit/context/create-function-complete.spec.ts @@ -2,6 +2,7 @@ import { WebClient } from '@slack/web-api'; import { assert } from '../helpers/assert'; import sinon from 'sinon'; import { createFunctionComplete } from '../../../src/context'; +import { describe, it } from 'node:test'; describe('createFunctionComplete', () => { it('complete should call functions.completeSuccess', async () => { diff --git a/test/unit/context/create-function-fail.spec.ts b/test/unit/context/create-function-fail.spec.ts index 9e1bca8df..259157e1f 100644 --- a/test/unit/context/create-function-fail.spec.ts +++ b/test/unit/context/create-function-fail.spec.ts @@ -2,6 +2,7 @@ import { WebClient } from '@slack/web-api'; import { assert } from '../helpers/assert'; import sinon from 'sinon'; import { createFunctionFail } from '../../../src/context'; +import { describe, it } from 'node:test'; describe('createFunctionFail', () => { it('fail should call functions.completeError', async () => { diff --git a/test/unit/context/create-respond.spec.ts b/test/unit/context/create-respond.spec.ts index 5b2363dc8..5bb5e1926 100644 --- a/test/unit/context/create-respond.spec.ts +++ b/test/unit/context/create-respond.spec.ts @@ -2,6 +2,7 @@ import type { AxiosInstance } from 'axios'; import { assert } from '../helpers/assert'; 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 3a9607c3b..53250a415 100644 --- a/test/unit/context/create-say-stream.spec.ts +++ b/test/unit/context/create-say-stream.spec.ts @@ -3,6 +3,7 @@ import { assert } from '../helpers/assert'; 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 8ee651a34..395f75530 100644 --- a/test/unit/context/create-say.spec.ts +++ b/test/unit/context/create-say.spec.ts @@ -2,6 +2,7 @@ import { WebClient } from '@slack/web-api'; import { assert } from '../helpers/assert'; 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 27f5a5d1d..15a5e718e 100644 --- a/test/unit/context/create-set-status.spec.ts +++ b/test/unit/context/create-set-status.spec.ts @@ -2,6 +2,7 @@ import { WebClient } from '@slack/web-api'; import { assert } from '../helpers/assert'; 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 d904601f4..49544e8b5 100644 --- a/test/unit/conversation-store.spec.ts +++ b/test/unit/conversation-store.spec.ts @@ -5,6 +5,7 @@ import { assert, AssertionError } from './helpers/assert'; 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 */ diff --git a/test/unit/errors.spec.ts b/test/unit/errors.spec.ts index e201d0d1e..924cacf24 100644 --- a/test/unit/errors.spec.ts +++ b/test/unit/errors.spec.ts @@ -10,6 +10,7 @@ import { UnknownError, asCodedError, } from '../../src/errors'; +import { describe, it } from 'node:test'; describe('Errors', () => { it('has errors matching codes', () => { diff --git a/test/unit/helpers.spec.ts b/test/unit/helpers.spec.ts index 816d47318..1faad33e4 100644 --- a/test/unit/helpers.spec.ts +++ b/test/unit/helpers.spec.ts @@ -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()', () => { diff --git a/test/unit/middleware/builtin.spec.ts b/test/unit/middleware/builtin.spec.ts index 71f95db8b..96f5319c8 100644 --- a/test/unit/middleware/builtin.spec.ts +++ b/test/unit/middleware/builtin.spec.ts @@ -16,6 +16,7 @@ import { proxyquire, wrapMiddleware, } from '../helpers'; +import { beforeEach, describe, it } from 'node:test'; interface DummyContext extends Context { matches?: RegExpExecArray; diff --git a/test/unit/node-test-globals.cjs b/test/unit/node-test-globals.cjs deleted file mode 100644 index cefa5c0c4..000000000 --- a/test/unit/node-test-globals.cjs +++ /dev/null @@ -1,8 +0,0 @@ -const test = require('node:test'); - -global.describe = test.describe; -global.it = test.it; -global.before = test.before; -global.after = test.after; -global.beforeEach = test.beforeEach; -global.afterEach = test.afterEach; diff --git a/test/unit/node-test-globals.d.ts b/test/unit/node-test-globals.d.ts deleted file mode 100644 index 0627c3c6e..000000000 --- a/test/unit/node-test-globals.d.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { - after as nodeAfter, - afterEach as nodeAfterEach, - before as nodeBefore, - beforeEach as nodeBeforeEach, - describe as nodeDescribe, - it as nodeIt, -} from 'node:test'; - -declare global { - const describe: typeof nodeDescribe; - const it: typeof nodeIt; - const before: typeof nodeBefore; - const after: typeof nodeAfter; - const beforeEach: typeof nodeBeforeEach; - const afterEach: typeof nodeAfterEach; -} - -export {}; diff --git a/test/unit/receivers/AwsLambdaReceiver.spec.ts b/test/unit/receivers/AwsLambdaReceiver.spec.ts index 55466f1aa..cea90a4d4 100644 --- a/test/unit/receivers/AwsLambdaReceiver.spec.ts +++ b/test/unit/receivers/AwsLambdaReceiver.spec.ts @@ -12,6 +12,7 @@ import { withNoopAppMetadata, withNoopWebClient, } from '../helpers'; +import { describe, it } from 'node:test'; const fakeAuthTestResponse = { ok: true, diff --git a/test/unit/receivers/ExpressReceiver.spec.ts b/test/unit/receivers/ExpressReceiver.spec.ts index 3bf32ce0b..81700a94d 100644 --- a/test/unit/receivers/ExpressReceiver.spec.ts +++ b/test/unit/receivers/ExpressReceiver.spec.ts @@ -30,6 +30,7 @@ import { withHttpCreateServer, withHttpsCreateServer, } from '../helpers'; +import { afterEach, beforeEach, after, describe, it } from 'node:test'; // Loading the system under test using overrides function importExpressReceiver( diff --git a/test/unit/receivers/HTTPModuleFunctions.spec.ts b/test/unit/receivers/HTTPModuleFunctions.spec.ts index 5c2937849..50d1eb2ca 100644 --- a/test/unit/receivers/HTTPModuleFunctions.spec.ts +++ b/test/unit/receivers/HTTPModuleFunctions.spec.ts @@ -2,6 +2,7 @@ import { createHmac } from 'node:crypto'; import { IncomingMessage, ServerResponse } from 'node:http'; import { assert } from '../helpers/assert'; import sinon from 'sinon'; +import { describe, it } from 'node:test'; import { AuthorizationError, HTTPReceiverDeferredRequestError, ReceiverMultipleAckError } from '../../../src/errors'; import type { BufferedIncomingMessage } from '../../../src/receivers/BufferedIncomingMessage'; diff --git a/test/unit/receivers/HTTPReceiver.spec.ts b/test/unit/receivers/HTTPReceiver.spec.ts index 444352915..75aec960e 100644 --- a/test/unit/receivers/HTTPReceiver.spec.ts +++ b/test/unit/receivers/HTTPReceiver.spec.ts @@ -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 { diff --git a/test/unit/receivers/HTTPResponseAck.spec.ts b/test/unit/receivers/HTTPResponseAck.spec.ts index 92913e1cb..54292b364 100644 --- a/test/unit/receivers/HTTPResponseAck.spec.ts +++ b/test/unit/receivers/HTTPResponseAck.spec.ts @@ -7,6 +7,7 @@ import * as HTTPModuleFunctions from '../../../src/receivers/HTTPModuleFunctions import { HTTPResponseAck } from '../../../src/receivers/HTTPResponseAck'; import type { ResponseAck } from '../../../src/types'; import { createFakeLogger } from '../helpers'; +import { afterEach, beforeEach, describe, it } from 'node:test'; describe('HTTPResponseAck', async () => { let setTimeoutSpy: sinon.SinonSpy; diff --git a/test/unit/receivers/SocketModeFunctions.spec.ts b/test/unit/receivers/SocketModeFunctions.spec.ts index fa99701dd..6323006d9 100644 --- a/test/unit/receivers/SocketModeFunctions.spec.ts +++ b/test/unit/receivers/SocketModeFunctions.spec.ts @@ -3,6 +3,7 @@ import { AuthorizationError, ReceiverMultipleAckError } from '../../../src/error 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 () => { diff --git a/test/unit/receivers/SocketModeReceiver.spec.ts b/test/unit/receivers/SocketModeReceiver.spec.ts index f1d836c54..6fdf932fd 100644 --- a/test/unit/receivers/SocketModeReceiver.spec.ts +++ b/test/unit/receivers/SocketModeReceiver.spec.ts @@ -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( diff --git a/test/unit/receivers/SocketModeResponseAck.spec.ts b/test/unit/receivers/SocketModeResponseAck.spec.ts index 4c04b9ed5..544cc3456 100644 --- a/test/unit/receivers/SocketModeResponseAck.spec.ts +++ b/test/unit/receivers/SocketModeResponseAck.spec.ts @@ -4,6 +4,7 @@ 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(); diff --git a/test/unit/receivers/verify-request.spec.ts b/test/unit/receivers/verify-request.spec.ts index 449067e61..4d3616d81 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 '../helpers/assert'; import { isValidSlackRequest, verifySlackRequest } from '../../../src/receivers/verify-request'; +import { describe, it } from 'node:test'; describe('Request verification', async () => { const signingSecret = 'secret'; From d131a5e04cef9aebf180539d95aed1f9fab3f1b8 Mon Sep 17 00:00:00 2001 From: "@theorderingmachine" Date: Wed, 8 Apr 2026 13:47:17 -0700 Subject: [PATCH 3/4] test: replace custom assert helper with direct imports --- AGENTS.md | 2 +- test/unit/App/basic.spec.ts | 78 ++++++--- test/unit/App/middlewares/arguments.spec.ts | 32 ++-- test/unit/App/middlewares/global.spec.ts | 46 ++--- test/unit/App/middlewares/listener.spec.ts | 6 +- test/unit/App/routing-action.spec.ts | 6 +- test/unit/App/routing-function.spec.ts | 6 +- test/unit/Assistant.spec.ts | 60 ++++--- test/unit/AssistantThreadContextStore.spec.ts | 18 +- test/unit/CustomFunction.spec.ts | 8 +- test/unit/WorkflowStep.spec.ts | 34 ++-- .../context/create-function-complete.spec.ts | 6 +- .../unit/context/create-function-fail.spec.ts | 6 +- test/unit/context/create-respond.spec.ts | 2 +- test/unit/context/create-say-stream.spec.ts | 2 +- test/unit/context/create-say.spec.ts | 2 +- test/unit/context/create-set-status.spec.ts | 2 +- test/unit/conversation-store.spec.ts | 20 +-- test/unit/errors.spec.ts | 4 +- test/unit/helpers.spec.ts | 46 ++--- test/unit/helpers/assert.ts | 163 ------------------ test/unit/middleware/builtin.spec.ts | 20 ++- test/unit/receivers/AwsLambdaReceiver.spec.ts | 6 +- test/unit/receivers/ExpressReceiver.spec.ts | 57 +++--- .../receivers/HTTPModuleFunctions.spec.ts | 46 ++--- test/unit/receivers/HTTPReceiver.spec.ts | 70 ++++---- test/unit/receivers/HTTPResponseAck.spec.ts | 38 +--- .../receivers/SocketModeFunctions.spec.ts | 6 +- .../unit/receivers/SocketModeReceiver.spec.ts | 78 ++++----- .../receivers/SocketModeResponseAck.spec.ts | 6 +- test/unit/receivers/verify-request.spec.ts | 32 ++-- 31 files changed, 385 insertions(+), 523 deletions(-) delete mode 100644 test/unit/helpers/assert.ts diff --git a/AGENTS.md b/AGENTS.md index d772f5bbf..c7b4cfb88 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -171,7 +171,7 @@ test/types/ # tsd type tests ### Test Conventions - **Test files** use `*.spec.ts` suffix -- **Assertions** use the local `test/unit/helpers/assert.ts` compatibility helper backed by Node.js `assert` and Sinon assertions +- **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** 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`. diff --git a/test/unit/App/basic.spec.ts b/test/unit/App/basic.spec.ts index ab52399b6..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 '../helpers/assert'; +import assert from 'node:assert/strict'; import sinon from 'sinon'; import { ErrorCode } from '../../../src/errors'; import SocketModeReceiver from '../../../src/receivers/SocketModeReceiver'; @@ -35,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); }); }); @@ -66,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); @@ -83,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); }); }); @@ -94,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'); }); }); @@ -115,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 () => { @@ -125,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); } }); @@ -136,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); } }); @@ -153,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); } }); @@ -172,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 () => { @@ -182,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 () => { @@ -221,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)); }); }); @@ -232,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 () => { @@ -246,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); } }); }); @@ -307,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); } }); }); @@ -337,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 d9b955659..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 '../../helpers/assert'; +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'; @@ -429,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'); @@ -546,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); }); @@ -573,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); }); @@ -635,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(); @@ -679,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); @@ -743,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(); }); @@ -773,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); @@ -802,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(); }); @@ -959,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 c9eccc2c4..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 '../../helpers/assert'; +import assert from 'node:assert/strict'; import sinon, { type SinonSpy } from 'sinon'; import type App from '../../../../src/App'; import type { ExtendedErrorHandlerArgs } from '../../../../src/App'; @@ -82,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 () => { @@ -104,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); }); @@ -124,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 () => { @@ -138,7 +142,7 @@ describe('App global middleware Processing', () => { }); await fakeReceiver.sendEvent(dummyReceiverEvent); - assert.isTrue(changed); + assert.strictEqual(changed, true); assert(fakeErrorHandler.notCalled); }); @@ -217,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); }); @@ -234,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); }); @@ -266,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); }); @@ -284,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'); }); @@ -303,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); }); @@ -321,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/listener.spec.ts b/test/unit/App/middlewares/listener.spec.ts index bd0724728..0a92cd50e 100644 --- a/test/unit/App/middlewares/listener.spec.ts +++ b/test/unit/App/middlewares/listener.spec.ts @@ -1,4 +1,4 @@ -import { assert } from '../../helpers/assert'; +import assert from 'node:assert/strict'; import sinon, { type SinonSpy } from 'sinon'; import type App from '../../../../src/App'; import { ErrorCode, isCodedError } from '../../../../src/errors'; @@ -57,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 7bdba3005..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 '../helpers/assert'; +import assert from 'node:assert/strict'; import sinon, { type SinonSpy } from 'sinon'; import type App from '../../../src/App'; import { @@ -119,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-function.spec.ts b/test/unit/App/routing-function.spec.ts index cfa49fdd4..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 '../helpers/assert'; +import assert from 'node:assert/strict'; import sinon from 'sinon'; import type App from '../../../src/App'; import { @@ -66,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/Assistant.spec.ts b/test/unit/Assistant.spec.ts index 76438b0b3..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 './helpers/assert'; +import assert from 'node:assert/strict'; import sinon from 'sinon'; import { type AllAssistantMiddlewareArgs, @@ -49,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', () => { @@ -134,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); }); }); @@ -149,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 () => { @@ -169,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); }); }); }); @@ -202,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 () => { @@ -216,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 () => { @@ -232,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 () => { @@ -248,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', () => { @@ -289,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 f23c4db53..def1aa34d 100644 --- a/test/unit/AssistantThreadContextStore.spec.ts +++ b/test/unit/AssistantThreadContextStore.spec.ts @@ -1,5 +1,5 @@ import type { WebClient } from '@slack/web-api'; -import { assert } from './helpers/assert'; +import assert from 'node:assert/strict'; import sinon from 'sinon'; import { extractThreadInfo } from '../../src/Assistant'; import { DefaultThreadContextStore } from '../../src/AssistantThreadContextStore'; @@ -43,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 () => { @@ -65,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 e9ffd93ce..b451f954c 100644 --- a/test/unit/CustomFunction.spec.ts +++ b/test/unit/CustomFunction.spec.ts @@ -1,4 +1,4 @@ -import { assert } from './helpers/assert'; +import assert from 'node:assert/strict'; import { CustomFunction, type SlackCustomFunctionMiddlewareArgs, @@ -20,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); }); }); @@ -46,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 9b71ad81d..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 './helpers/assert'; +import assert from 'node:assert/strict'; import sinon from 'sinon'; import { type AllWorkflowStepMiddlewareArgs, @@ -38,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); }); }); @@ -153,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 () => { @@ -165,7 +165,7 @@ describe('WorkflowStep class', () => { const { isStepEvent } = importWorkflowStep(); const actionIsStepEvent = isStepEvent(fakeEditArgs); - assert.isFalse(actionIsStepEvent); + assert.strictEqual(actionIsStepEvent, false); }); }); @@ -182,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 () => { @@ -193,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 () => { @@ -205,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 () => { @@ -217,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 70cd6b1e0..b4a58e1b3 100644 --- a/test/unit/context/create-function-complete.spec.ts +++ b/test/unit/context/create-function-complete.spec.ts @@ -1,5 +1,5 @@ import { WebClient } from '@slack/web-api'; -import { assert } from '../helpers/assert'; +import assert from 'node:assert/strict'; import sinon from 'sinon'; import { createFunctionComplete } from '../../../src/context'; import { describe, it } from 'node:test'; @@ -29,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 259157e1f..68cf32447 100644 --- a/test/unit/context/create-function-fail.spec.ts +++ b/test/unit/context/create-function-fail.spec.ts @@ -1,5 +1,5 @@ import { WebClient } from '@slack/web-api'; -import { assert } from '../helpers/assert'; +import assert from 'node:assert/strict'; import sinon from 'sinon'; import { createFunctionFail } from '../../../src/context'; import { describe, it } from 'node:test'; @@ -29,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 5bb5e1926..aea2ed974 100644 --- a/test/unit/context/create-respond.spec.ts +++ b/test/unit/context/create-respond.spec.ts @@ -1,5 +1,5 @@ import type { AxiosInstance } from 'axios'; -import { assert } from '../helpers/assert'; +import assert from 'node:assert/strict'; import sinon from 'sinon'; import { createRespond } from '../../../src/context'; import { describe, it } from 'node:test'; diff --git a/test/unit/context/create-say-stream.spec.ts b/test/unit/context/create-say-stream.spec.ts index 53250a415..ba99fee19 100644 --- a/test/unit/context/create-say-stream.spec.ts +++ b/test/unit/context/create-say-stream.spec.ts @@ -1,5 +1,5 @@ import { WebClient } from '@slack/web-api'; -import { assert } from '../helpers/assert'; +import assert from 'node:assert/strict'; import sinon from 'sinon'; import { createSayStream } from '../../../src/context'; import type { Context } from '../../../src/types'; diff --git a/test/unit/context/create-say.spec.ts b/test/unit/context/create-say.spec.ts index 395f75530..6cdef3a51 100644 --- a/test/unit/context/create-say.spec.ts +++ b/test/unit/context/create-say.spec.ts @@ -1,5 +1,5 @@ import { WebClient } from '@slack/web-api'; -import { assert } from '../helpers/assert'; +import assert from 'node:assert/strict'; import sinon from 'sinon'; import { createSay } from '../../../src/context'; import { describe, it } from 'node:test'; diff --git a/test/unit/context/create-set-status.spec.ts b/test/unit/context/create-set-status.spec.ts index 15a5e718e..c55a50787 100644 --- a/test/unit/context/create-set-status.spec.ts +++ b/test/unit/context/create-set-status.spec.ts @@ -1,5 +1,5 @@ import { WebClient } from '@slack/web-api'; -import { assert } from '../helpers/assert'; +import assert from 'node:assert/strict'; import sinon from 'sinon'; import { createSetStatus } from '../../../src/context'; import { afterEach, beforeEach, describe, it } from 'node:test'; diff --git a/test/unit/conversation-store.spec.ts b/test/unit/conversation-store.spec.ts index 49544e8b5..77977d34f 100644 --- a/test/unit/conversation-store.spec.ts +++ b/test/unit/conversation-store.spec.ts @@ -1,7 +1,7 @@ import path from 'node:path'; import type { Logger } from '@slack/logger'; import type { WebClient } from '@slack/web-api'; -import { assert, AssertionError } from './helpers/assert'; +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'; @@ -70,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 () => { @@ -101,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(); } @@ -137,7 +137,7 @@ describe('conversationContext middleware', () => { // Assert assert(fakeNext.called); - assert.notProperty(dummyContext, 'conversation'); + assert.ok(!('conversation' in dummyContext)); // NOTE: node:assert types do not offer assertion signatures here. if (dummyContext.updateConversation === undefined) { assert.fail(); @@ -196,7 +196,7 @@ describe('MemoryStore', () => { const store = new MemoryStore(); // Assert - assert.isOk(store); + assert.ok(store); }); }); @@ -230,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)); } }); @@ -251,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 924cacf24..d07050495 100644 --- a/test/unit/errors.spec.ts +++ b/test/unit/errors.spec.ts @@ -1,4 +1,4 @@ -import { assert } from './helpers/assert'; +import assert from 'node:assert/strict'; import { AppInitializationError, AuthorizationError, @@ -29,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 1faad33e4..8ce202062 100644 --- a/test/unit/helpers.spec.ts +++ b/test/unit/helpers.spec.ts @@ -1,4 +1,4 @@ -import { assert } from './helpers/assert'; +import assert from 'node:assert/strict'; import { IncomingEventType, extractEventChannelId, @@ -117,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'); + } }); }); }); @@ -237,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); }); }); @@ -330,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); }); } }); @@ -358,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); }); } }); @@ -409,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/helpers/assert.ts b/test/unit/helpers/assert.ts deleted file mode 100644 index c4448e7a8..000000000 --- a/test/unit/helpers/assert.ts +++ /dev/null @@ -1,163 +0,0 @@ -import { AssertionError } from 'node:assert'; -import nodeAssert from 'node:assert/strict'; -import sinon from 'sinon'; - -type AssertFn = ((value: unknown, message?: string) => asserts value) & { - ok: typeof nodeAssert.ok; - fail: typeof nodeAssert.fail; - equal: typeof nodeAssert.equal; - notEqual: typeof nodeAssert.notEqual; - strictEqual: typeof nodeAssert.strictEqual; - deepEqual: typeof nodeAssert.deepEqual; - deepStrictEqual: typeof nodeAssert.deepStrictEqual; - throws: typeof nodeAssert.throws; - exists(value: unknown, message?: string): void; - notExists(value: unknown, message?: string): void; - isDefined(value: unknown, message?: string): void; - isUndefined(value: unknown, message?: string): void; - isTrue(value: unknown, message?: string): void; - isFalse(value: unknown, message?: string): void; - isOk(value: unknown, message?: string): void; - isNotNull(value: unknown, message?: string): void; - isArray(value: unknown, message?: string): void; - isFunction(value: unknown, message?: string): void; - isAtLeast(value: number, min: number, message?: string): void; - isEmpty(value: unknown, message?: string): void; - lengthOf(value: { length: number }, expected: number, message?: string): void; - typeOf(value: unknown, expected: string, message?: string): void; - instanceOf(value: unknown, ctor: new (...args: any[]) => any, message?: string): void; - notInstanceOf(value: unknown, ctor: new (...args: any[]) => any, message?: string): void; - property(object: object, prop: PropertyKey, message?: string): void; - notProperty(object: object, prop: PropertyKey, message?: string): void; - propertyVal(object: unknown, prop: PropertyKey, expected: unknown, message?: string): void; - sameMembers(actual: unknown[], expected: unknown[], message?: string): void; - called(spy: sinon.SinonSpy, message?: string): void; - notCalled(spy: sinon.SinonSpy, message?: string): void; - calledOnce(spy: sinon.SinonSpy, message?: string): void; - calledTwice(spy: sinon.SinonSpy, message?: string): void; - calledThrice(spy: sinon.SinonSpy, message?: string): void; - callCount(spy: sinon.SinonSpy, count: number, message?: string): void; - calledWith(spy: sinon.SinonSpy, ...args: unknown[]): void; - calledWithMatch(spy: sinon.SinonSpy, ...args: unknown[]): void; - calledOnceWithExactly(spy: sinon.SinonSpy, ...args: unknown[]): void; - neverCalledWith(spy: sinon.SinonSpy, ...args: unknown[]): void; -}; - -const assert: AssertFn = Object.assign( - (value: unknown, message?: string): asserts value => nodeAssert.ok(value, message), - { - ok: nodeAssert.ok, - fail: nodeAssert.fail, - equal: nodeAssert.equal, - notEqual: nodeAssert.notEqual, - strictEqual: nodeAssert.strictEqual, - deepEqual: nodeAssert.deepEqual, - deepStrictEqual: nodeAssert.deepStrictEqual, - throws: nodeAssert.throws, - - exists(value: unknown, message?: string): void { - nodeAssert.notEqual(value, null, message); - }, - notExists(value: unknown, message?: string): void { - nodeAssert.equal(value, null, message); - }, - isDefined(value: unknown, message?: string): void { - nodeAssert.notStrictEqual(value, undefined, message); - }, - isUndefined(value: unknown, message?: string): void { - nodeAssert.strictEqual(value, undefined, message); - }, - isTrue(value: unknown, message?: string): void { - nodeAssert.strictEqual(value, true, message); - }, - isFalse(value: unknown, message?: string): void { - nodeAssert.strictEqual(value, false, message); - }, - isOk(value: unknown, message?: string): void { - nodeAssert.ok(value, message); - }, - isNotNull(value: unknown, message?: string): void { - nodeAssert.notStrictEqual(value, null, message); - }, - isArray(value: unknown, message?: string): void { - nodeAssert.ok(Array.isArray(value), message ?? 'expected value to be an array'); - }, - isFunction(value: unknown, message?: string): void { - nodeAssert.strictEqual(typeof value, 'function', message); - }, - isAtLeast(value: number, min: number, message?: string): void { - nodeAssert.ok(value >= min, message ?? `expected ${value} to be at least ${min}`); - }, - isEmpty(value: unknown, message?: string): void { - if (Array.isArray(value) || typeof value === 'string') { - nodeAssert.strictEqual(value.length, 0, message); - return; - } - if (value && typeof value === 'object') { - nodeAssert.strictEqual(Object.keys(value).length, 0, message); - return; - } - nodeAssert.fail(message ?? 'expected value to be empty'); - }, - lengthOf(value: { length: number }, expected: number, message?: string): void { - nodeAssert.strictEqual(value.length, expected, message); - }, - typeOf(value: unknown, expected: string, message?: string): void { - nodeAssert.strictEqual(typeof value, expected, message); - }, - instanceOf(value: unknown, ctor: new (...args: any[]) => any, message?: string): void { - nodeAssert.ok(value instanceof ctor, message); - }, - notInstanceOf(value: unknown, ctor: new (...args: any[]) => any, message?: string): void { - nodeAssert.ok(!(value instanceof ctor), message); - }, - property(object: object, prop: PropertyKey, message?: string): void { - nodeAssert.ok(prop in object, message); - }, - notProperty(object: object, prop: PropertyKey, message?: string): void { - nodeAssert.ok(!(prop in object), message); - }, - propertyVal(object: unknown, prop: PropertyKey, expected: unknown, message?: string): void { - nodeAssert.ok(object && typeof object === 'object', message ?? 'expected value to be an object'); - nodeAssert.ok(prop in object, message); - nodeAssert.deepStrictEqual((object as Record)[prop], expected, message); - }, - sameMembers(actual: unknown[], expected: unknown[], message?: string): void { - const normalize = (arr: unknown[]) => [...arr].sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b))); - nodeAssert.deepStrictEqual(normalize(actual), normalize(expected), message); - }, - - called(spy: sinon.SinonSpy): void { - sinon.assert.called(spy); - }, - notCalled(spy: sinon.SinonSpy): void { - sinon.assert.notCalled(spy); - }, - calledOnce(spy: sinon.SinonSpy): void { - sinon.assert.calledOnce(spy); - }, - calledTwice(spy: sinon.SinonSpy): void { - sinon.assert.calledTwice(spy); - }, - calledThrice(spy: sinon.SinonSpy): void { - sinon.assert.calledThrice(spy); - }, - callCount(spy: sinon.SinonSpy, count: number): void { - sinon.assert.callCount(spy, count); - }, - calledWith(spy: sinon.SinonSpy, ...args: unknown[]): void { - sinon.assert.calledWith(spy, ...args); - }, - calledWithMatch(spy: sinon.SinonSpy, ...args: unknown[]): void { - sinon.assert.calledWithMatch(spy, ...args); - }, - calledOnceWithExactly(spy: sinon.SinonSpy, ...args: unknown[]): void { - sinon.assert.calledOnceWithExactly(spy, ...args); - }, - neverCalledWith(spy: sinon.SinonSpy, ...args: unknown[]): void { - sinon.assert.neverCalledWith(spy, ...args); - }, - }, -) as AssertFn; - -export { assert, AssertionError }; diff --git a/test/unit/middleware/builtin.spec.ts b/test/unit/middleware/builtin.spec.ts index 96f5319c8..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 '../helpers/assert'; +import assert from 'node:assert/strict'; import sinon from 'sinon'; import { expectType } from 'tsd'; import { ErrorCode } from '../../../src/errors'; @@ -53,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(); } @@ -73,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)); }; } @@ -162,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 () => { @@ -418,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 () => { @@ -432,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 cea90a4d4..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 '../helpers/assert'; +import assert from 'node:assert/strict'; import sinon from 'sinon'; import AwsLambdaReceiver from '../../../src/receivers/AwsLambdaReceiver'; import { @@ -31,7 +31,7 @@ describe('AwsLambdaReceiver', () => { signingSecret: 'my-secret', logger: noopLogger, }); - assert.isNotNull(awsReceiver); + assert.notStrictEqual(awsReceiver, null); }); it('should have start method', async () => { @@ -40,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 81700a94d..8754f9316 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 '../helpers/assert'; +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'; @@ -87,7 +87,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 = { @@ -114,7 +114,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); @@ -139,10 +139,9 @@ describe('ExpressReceiver', () => { redirectUri, installerOptions, }); - assert.isNotNull(receiver); + assert.notStrictEqual(receiver, null); // missing redirectUriPath - assert.throws( - () => + assert.throws(() => new ExpressReceiver({ clientId, clientSecret, @@ -150,12 +149,9 @@ describe('ExpressReceiver', () => { stateSecret, scopes, redirectUri, - }), - AppInitializationError, - ); + }), AppInitializationError); // inconsistent redirectUriPath - assert.throws( - () => + assert.throws(() => new ExpressReceiver({ clientId: 'my-clientId', clientSecret, @@ -166,12 +162,9 @@ describe('ExpressReceiver', () => { installerOptions: { redirectUriPath: '/hiya', }, - }), - AppInitializationError, - ); + }), AppInitializationError); // inconsistent redirectUri - assert.throws( - () => + assert.throws(() => new ExpressReceiver({ clientId: 'my-clientId', clientSecret, @@ -180,9 +173,7 @@ describe('ExpressReceiver', () => { scopes, redirectUri: 'http://example.com/hiya', installerOptions, - }), - AppInitializationError, - ); + }), AppInitializationError); }); }); @@ -231,7 +222,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); @@ -250,8 +241,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); @@ -266,8 +259,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); }); }); @@ -291,8 +286,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( @@ -315,7 +312,7 @@ 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()'); }); }); @@ -613,21 +610,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 50d1eb2ca..883501178 100644 --- a/test/unit/receivers/HTTPModuleFunctions.spec.ts +++ b/test/unit/receivers/HTTPModuleFunctions.spec.ts @@ -1,6 +1,6 @@ import { createHmac } from 'node:crypto'; import { IncomingMessage, ServerResponse } from 'node:http'; -import { assert } from '../helpers/assert'; +import assert from 'node:assert/strict'; import sinon from 'sinon'; import { describe, it } from 'node:test'; @@ -15,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; @@ -34,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; @@ -78,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 () => { @@ -107,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'; @@ -128,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 () => { @@ -151,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 () => { @@ -165,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'; @@ -183,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'); } }); }); @@ -193,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); }); }); @@ -227,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; @@ -238,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); }); }); @@ -253,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; @@ -265,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); }); }); @@ -278,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 75aec960e..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 '../helpers/assert'; +import assert from 'node:assert/strict'; import type { ParamsDictionary } from 'express-serve-static-core'; import { match } from 'path-to-regexp'; import sinon from 'sinon'; @@ -87,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 () => { @@ -96,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, @@ -113,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 () => { @@ -138,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, @@ -149,12 +154,9 @@ describe('HTTPReceiver', () => { stateSecret, scopes, redirectUri, - }), - AppInitializationError, - ); + }), AppInitializationError); // inconsistent redirectUriPath - assert.throws( - () => + assert.throws(() => new HTTPReceiver({ clientId: 'my-clientId', clientSecret, @@ -165,12 +167,9 @@ describe('HTTPReceiver', () => { installerOptions: { redirectUriPath: '/hiya', }, - }), - AppInitializationError, - ); + }), AppInitializationError); // inconsistent redirectUri - assert.throws( - () => + assert.throws(() => new HTTPReceiver({ clientId: 'my-clientId', clientSecret, @@ -179,9 +178,7 @@ describe('HTTPReceiver', () => { scopes, redirectUri: 'http://example.com/hiya', installerOptions, - }), - AppInitializationError, - ); + }), AppInitializationError); }); }); describe('start() method', () => { @@ -191,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(); @@ -225,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'; @@ -257,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'; @@ -289,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'; @@ -327,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'; @@ -371,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'; @@ -560,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); }); }); @@ -599,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 54292b364..844585876 100644 --- a/test/unit/receivers/HTTPResponseAck.spec.ts +++ b/test/unit/receivers/HTTPResponseAck.spec.ts @@ -1,5 +1,5 @@ import { IncomingMessage, ServerResponse } from 'node:http'; -import { assert } from '../helpers/assert'; +import assert from 'node:assert/strict'; import sinon from 'sinon'; import { expectType } from 'tsd'; import { ReceiverMultipleAckError } from '../../../src/errors'; @@ -31,8 +31,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 }); @@ -47,11 +47,7 @@ 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', async () => { const httpRequest = sinon.createStubInstance(IncomingMessage) as IncomingMessage; @@ -66,11 +62,7 @@ describe('HTTPResponseAck', async () => { httpRequest, httpResponse, }); - assert.equal( - setTimeoutSpy.firstCall.args[1], - unhandledRequestTimeoutMillis, - `a ${unhandledRequestTimeoutMillis} timeout for the unhandledRequestHandler callback is expected`, - ); + 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); }); @@ -105,7 +97,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 () => { @@ -134,11 +126,7 @@ 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'); @@ -169,11 +157,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; @@ -186,10 +170,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 6323006d9..1514f8854 100644 --- a/test/unit/receivers/SocketModeFunctions.spec.ts +++ b/test/unit/receivers/SocketModeFunctions.spec.ts @@ -1,4 +1,4 @@ -import { assert } from '../helpers/assert'; +import assert from 'node:assert/strict'; import { AuthorizationError, ReceiverMultipleAckError } from '../../../src/errors'; import { defaultProcessEventErrorHandler } from '../../../src/receivers/SocketModeFunctions'; import type { ReceiverEvent } from '../../../src/types'; @@ -20,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 = { @@ -32,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 6fdf932fd..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 '../helpers/assert'; +import assert from 'node:assert/strict'; import type { ParamsDictionary } from 'express-serve-static-core'; import { match } from 'path-to-regexp'; import sinon from 'sinon'; @@ -67,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); @@ -86,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); @@ -96,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(); @@ -118,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); @@ -143,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); @@ -171,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); @@ -198,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); @@ -228,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, @@ -239,12 +238,9 @@ describe('SocketModeReceiver', () => { stateSecret, scopes, redirectUri, - }), - AppInitializationError, - ); + }), AppInitializationError); // inconsistent redirectUriPath - assert.throws( - () => + assert.throws(() => new SocketModeReceiver({ appToken, clientId: 'my-clientId', @@ -255,12 +251,9 @@ describe('SocketModeReceiver', () => { installerOptions: { redirectUriPath: '/hiya', }, - }), - AppInitializationError, - ); + }), AppInitializationError); // inconsistent redirectUri - assert.throws( - () => + assert.throws(() => new SocketModeReceiver({ appToken, clientId: 'my-clientId', @@ -269,9 +262,7 @@ describe('SocketModeReceiver', () => { scopes, redirectUri: 'http://example.com/hiya', installerOptions, - }), - AppInitializationError, - ); + }), AppInitializationError); }); }); describe('request handling', () => { @@ -300,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'; @@ -332,7 +323,7 @@ describe('SocketModeReceiver', () => { userScopes, }, }); - assert.isNotNull(receiver); + assert.notStrictEqual(receiver, null); receiver.installer = installProviderStub as unknown as InstallProvider; const fakeReq = { url: '/hiya', @@ -367,7 +358,7 @@ describe('SocketModeReceiver', () => { userScopes, }, }); - assert.isNotNull(receiver); + assert.notStrictEqual(receiver, null); receiver.installer = installProviderStub as unknown as InstallProvider; const fakeReq = { url: '/hiya', @@ -402,7 +393,7 @@ describe('SocketModeReceiver', () => { userScopes, }, }); - assert.isNotNull(receiver); + assert.notStrictEqual(receiver, null); receiver.installer = installProviderStub as unknown as InstallProvider; const fakeReq = { url: '/hiya', @@ -441,7 +432,7 @@ describe('SocketModeReceiver', () => { callbackOptions, }, }); - assert.isNotNull(receiver); + assert.notStrictEqual(receiver, null); receiver.installer = installProviderStub as unknown as InstallProvider; const fakeReq = { url: '/heyo', @@ -491,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'; @@ -519,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); @@ -557,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); @@ -595,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); @@ -636,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); @@ -679,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); @@ -713,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); }); }); }); @@ -738,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); @@ -762,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 544cc3456..6a3743e89 100644 --- a/test/unit/receivers/SocketModeResponseAck.spec.ts +++ b/test/unit/receivers/SocketModeResponseAck.spec.ts @@ -1,4 +1,4 @@ -import { assert } from '../helpers/assert'; +import assert from 'node:assert/strict'; import sinon from 'sinon'; import { expectType } from 'tsd'; import { SocketModeResponseAck } from '../../../src/receivers/SocketModeResponseAck'; @@ -19,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 4d3616d81..3153d2089 100644 --- a/test/unit/receivers/verify-request.spec.ts +++ b/test/unit/receivers/verify-request.spec.ts @@ -1,5 +1,5 @@ import { createHmac } from 'node:crypto'; -import { assert } from '../helpers/assert'; +import assert from 'node:assert/strict'; import { isValidSlackRequest, verifySlackRequest } from '../../../src/receivers/verify-request'; import { describe, it } from 'node:test'; @@ -38,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 () => { @@ -58,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'); } }); }); @@ -70,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 @@ -87,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); }); }); }); From dfd6928e6b74d0bd060cfce40ff4d28b8f20cda6 Mon Sep 17 00:00:00 2001 From: "@theorderingmachine" Date: Wed, 8 Apr 2026 13:50:47 -0700 Subject: [PATCH 4/4] test: use tsx for TypeScript test execution --- .github/maintainers_guide.md | 2 +- .vscode/launch.json | 9 +++---- package.json | 7 +++-- test/unit/.mocharc.json | 5 ---- test/unit/receivers/ExpressReceiver.spec.ts | 29 ++++++++++++--------- test/unit/receivers/HTTPResponseAck.spec.ts | 18 ++++++++++--- 6 files changed, 38 insertions(+), 32 deletions(-) delete mode 100644 test/unit/.mocharc.json diff --git a/.github/maintainers_guide.md b/.github/maintainers_guide.md index dcf7429a5..bd15836a9 100644 --- a/.github/maintainers_guide.md +++ b/.github/maintainers_guide.md @@ -21,7 +21,7 @@ Test code should be written in syntax that runs on the oldest supported Node.js #### 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 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 --require ts-node/register --require source-map-support/register --test test/unit/{test-name}.spec.ts` (replace `{test-name}` with an actual test file path). +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 7fa7d5135..c1ebc81b6 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -12,18 +12,15 @@ "stopOnEntry": false, "args": [ "--inspect-brk", - "--require", - "ts-node/register", - "--require", - "source-map-support/register", + "--import", + "tsx", "--test", "test/unit/**/*.spec.ts" ], "cwd": "${workspaceFolder}", "env": { "NODE_ENV": "testing", - "TS_NODE_FILES": "true", - "TS_NODE_PROJECT": "tsconfig.test.json" + "TSX_TSCONFIG_PATH": "tsconfig.test.json" }, "skipFiles": ["/**"] } diff --git a/package.json b/package.json index 122ee265d..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.test.json TS_NODE_FILES=true node --require ts-node/register --require source-map-support/register --test test/unit/**/*.spec.ts", - "test:coverage": "TS_NODE_PROJECT=tsconfig.test.json TS_NODE_FILES=true node --experimental-test-coverage --require ts-node/register --require source-map-support/register --test test/unit/**/*.spec.ts", + "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" }, @@ -68,9 +68,8 @@ "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/receivers/ExpressReceiver.spec.ts b/test/unit/receivers/ExpressReceiver.spec.ts index 8754f9316..fdad444a0 100644 --- a/test/unit/receivers/ExpressReceiver.spec.ts +++ b/test/unit/receivers/ExpressReceiver.spec.ts @@ -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,7 +29,7 @@ import { withHttpCreateServer, withHttpsCreateServer, } from '../helpers'; -import { afterEach, beforeEach, after, describe, it } from 'node:test'; +import { afterEach, beforeEach, describe, it } from 'node:test'; // Loading the system under test using overrides function importExpressReceiver( @@ -318,10 +317,10 @@ describe('ExpressReceiver', () => { }); 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 () { @@ -329,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: '' }); diff --git a/test/unit/receivers/HTTPResponseAck.spec.ts b/test/unit/receivers/HTTPResponseAck.spec.ts index 844585876..feee9af8b 100644 --- a/test/unit/receivers/HTTPResponseAck.spec.ts +++ b/test/unit/receivers/HTTPResponseAck.spec.ts @@ -1,14 +1,19 @@ import { IncomingMessage, ServerResponse } from 'node:http'; +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; @@ -129,10 +134,15 @@ describe('HTTPResponseAck', async () => { 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,