Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/maintainers_guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ All you need to work with this project is a supported version of [Node.js](https

#### Unit Tests

This package has unit tests for most files in the same directory the code is in with the suffix `.spec` (i.e. `exampleFile.spec.ts`). You can run the entire test suite using the npm script `npm test`. This command is also executed by GitHub Actions, the continuous integration service, for every Pull Request and branch. The coverage is computed with the `codecov` package. The tests themselves are run using the `mocha` test runner.
This package has unit tests for most files in the same directory the code is in with the suffix `.spec` (i.e. `exampleFile.spec.ts`). You can run the entire test suite using the npm script `npm test`. This command is also executed by GitHub Actions, the continuous integration service, for every Pull Request and branch. Coverage is collected with Node.js's built-in test coverage support and uploaded by CI. The tests themselves are run using Node.js's built-in test runner.

Test code should be written in syntax that runs on the oldest supported Node.js version. This ensures that backwards compatibility is tested and the APIs look reasonable in versions of Node.js that do not support the most modern syntax.

#### Debugging

A useful trick for debugging inside tests is to use the Chrome Debugging Protocol feature of Node.js to set breakpoints and interactively debug. In order to do this you must run mocha directly. This means that you should have already linted the source (`npm run lint`), manually. You then run the tests using the following command: `./node_modules/.bin/mocha test/{test-name}.js --debug-brk --inspect` (replace {test-name} with an actual test file).
A useful trick for debugging inside tests is to use the Chrome Debugging Protocol feature of Node.js to set breakpoints and interactively debug. In order to do this you should have already linted the source (`npm run lint`), manually. You can then run a specific test file with Node.js's test runner, for example: `node --inspect-brk --import tsx --test test/unit/{test-name}.spec.ts` (replace `{test-name}` with an actual test file path).

#### Local Development

Expand Down
13 changes: 9 additions & 4 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,19 @@
"type": "node",
"request": "launch",
"name": "Spec tests",
"program": "${workspaceFolder}/node_modules/mocha/bin/_mocha",
"runtimeExecutable": "node",
"stopOnEntry": false,
"args": ["--config", ".mocharc.json", "--no-timeouts", "src/*.spec.ts", "src/**/*.spec.ts"],
"args": [
"--inspect-brk",
"--import",
"tsx",
"--test",
"test/unit/**/*.spec.ts"
],
"cwd": "${workspaceFolder}",
"runtimeExecutable": null,
"env": {
"NODE_ENV": "testing",
"TS_NODE_PROJECT": "tsconfig.test.json"
"TSX_TSCONFIG_PATH": "tsconfig.test.json"
},
"skipFiles": ["<node_internals>/**"]
}
Expand Down
8 changes: 4 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
```
Expand Down Expand Up @@ -171,9 +171,9 @@ test/types/ # tsd type tests
### Test Conventions

- **Test files** use `*.spec.ts` suffix
- **Assertions** use chai (`expect`, `assert`)
- **Assertions** in tests use direct `node:assert/strict` and `sinon.assert` imports
- **Mocking** uses sinon (`stub`, `spy`, `fake`) and proxyquire for module-level dependency replacement
- **Test config** in `test/unit/.mocharc.json`
- **Test config** lives in `package.json` scripts plus direct `node:test` imports in the spec files
- **Where to put new tests:** Mirror the source structure. For `src/Foo.ts`, add `test/unit/Foo.spec.ts`. For `src/receivers/Bar.ts`, add `test/unit/receivers/Bar.spec.ts`.

### CI
Expand Down
12 changes: 3 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@
"lint": "npx @biomejs/biome check docs src test examples",
"lint:fix": "npx @biomejs/biome check --write docs src test examples",
"test": "npm run build && npm run lint && npm run test:types && npm run test:coverage",
"test:unit": "TS_NODE_PROJECT=tsconfig.json mocha --config test/unit/.mocharc.json",
"test:coverage": "c8 npm run test:unit",
"test:unit": "TSX_TSCONFIG_PATH=tsconfig.test.json tsx --test test/unit/**/*.spec.ts",
"test:coverage": "TSX_TSCONFIG_PATH=tsconfig.test.json node --experimental-test-coverage --import tsx --test test/unit/**/*.spec.ts",
"test:types": "tsd --files test/types",
"watch": "npx nodemon --watch 'src' --ext 'ts' --exec npm run build"
},
Expand Down Expand Up @@ -61,21 +61,15 @@
"@biomejs/biome": "^1.9.0",
"@changesets/cli": "^2.29.8",
"@tsconfig/node18": "^18.2.4",
"@types/chai": "^4.1.7",
"@types/mocha": "^10.0.1",
"@types/node": "18.19.130",
"@types/proxyquire": "^1.3.31",
"@types/sinon": "^17.0.4",
"@types/tsscmp": "^1.0.0",
"c8": "^10.1.2",
"chai": "~4.3.0",
"mocha": "^10.2.0",
"proxyquire": "^2.1.3",
"shx": "^0.3.2",
"sinon": "^20.0.0",
"source-map-support": "^0.5.12",
"ts-node": "^10.9.2",
"tsd": "^0.31.2",
"tsx": "^4.20.6",
"typescript": "5.3.3"
},
"peerDependencies": {
Expand Down
5 changes: 0 additions & 5 deletions test/unit/.mocharc.json

This file was deleted.

79 changes: 54 additions & 25 deletions test/unit/App/basic.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { LogLevel } from '@slack/logger';
import { assert } from 'chai';
import assert from 'node:assert/strict';
import sinon from 'sinon';
import { ErrorCode } from '../../../src/errors';
import SocketModeReceiver from '../../../src/receivers/SocketModeReceiver';
Expand All @@ -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';
Expand All @@ -34,13 +35,17 @@ describe('App basic features', () => {
const MockApp = importApp(overrides);
const app = new MockApp({ token: '', signingSecret: '', port: 9999 });
// biome-ignore lint/complexity/useLiteralKeys: reaching into private fields
assert.propertyVal(app['receiver'], 'port', 9999);
assert.ok(app['receiver'] && typeof app['receiver'] === 'object');
assert.ok('port' in app['receiver']);
assert.deepStrictEqual((app['receiver'] as unknown as Record<PropertyKey, unknown>)['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<PropertyKey, unknown>)['port'], 9999);
});
});

Expand All @@ -65,7 +70,9 @@ describe('App basic features', () => {
installationStore,
});
// biome-ignore lint/complexity/useLiteralKeys: reaching into private fields
assert.propertyVal(app['receiver'], 'httpServerPort', 9999);
assert.ok(app['receiver'] && typeof app['receiver'] === 'object');
assert.ok('httpServerPort' in app['receiver']);
assert.deepStrictEqual((app['receiver'] as unknown as Record<PropertyKey, unknown>)['httpServerPort'], 9999);
});
it('should accept a port value under installerOptions', async () => {
const MockApp = importApp(overrides);
Expand All @@ -82,7 +89,9 @@ describe('App basic features', () => {
installationStore,
});
// biome-ignore lint/complexity/useLiteralKeys: reaching into private fields
assert.propertyVal(app['receiver'], 'httpServerPort', 9999);
assert.ok(app['receiver'] && typeof app['receiver'] === 'object');
assert.ok('httpServerPort' in app['receiver']);
assert.deepStrictEqual((app['receiver'] as unknown as Record<PropertyKey, unknown>)['httpServerPort'], 9999);
});
});

Expand All @@ -93,12 +102,12 @@ describe('App basic features', () => {
const MockApp = importApp(overrides);
const app = new MockApp({ token: '', signingSecret: '' });
// TODO: verify that the fake bot ID and fake bot user ID are retrieved
assert.instanceOf(app, MockApp);
assert.ok(app instanceof MockApp);
});
it('should pass the given token to app.client', async () => {
const MockApp = importApp(overrides);
const app = new MockApp({ token: 'xoxb-foo-bar', signingSecret: '' });
assert.isDefined(app.client);
assert.notStrictEqual(app.client, undefined);
assert.equal(app.client.token, 'xoxb-foo-bar');
});
});
Expand All @@ -114,7 +123,9 @@ describe('App basic features', () => {
new MockApp({ signingSecret: '' });
assert.fail();
} catch (error) {
assert.propertyVal(error, 'code', ErrorCode.AppInitializationError);
assert.ok(error && typeof error === 'object');
assert.ok('code' in error);
assert.deepStrictEqual((error as unknown as Record<PropertyKey, unknown>)['code'], ErrorCode.AppInitializationError);
}
});
it('should fail when both a token and authorize callback are specified', async () => {
Expand All @@ -124,7 +135,9 @@ describe('App basic features', () => {
new MockApp({ token: '', authorize: authorizeCallback, signingSecret: '' });
assert.fail();
} catch (error) {
assert.propertyVal(error, 'code', ErrorCode.AppInitializationError);
assert.ok(error && typeof error === 'object');
assert.ok('code' in error);
assert.deepStrictEqual((error as unknown as Record<PropertyKey, unknown>)['code'], ErrorCode.AppInitializationError);
assert(authorizeCallback.notCalled);
}
});
Expand All @@ -135,7 +148,9 @@ describe('App basic features', () => {
new MockApp({ token: '', clientId: '', clientSecret: '', stateSecret: '', signingSecret: '' });
assert.fail();
} catch (error) {
assert.propertyVal(error, 'code', ErrorCode.AppInitializationError);
assert.ok(error && typeof error === 'object');
assert.ok('code' in error);
assert.deepStrictEqual((error as unknown as Record<PropertyKey, unknown>)['code'], ErrorCode.AppInitializationError);
assert(authorizeCallback.notCalled);
}
});
Expand All @@ -152,7 +167,9 @@ describe('App basic features', () => {
});
assert.fail();
} catch (error) {
assert.propertyVal(error, 'code', ErrorCode.AppInitializationError);
assert.ok(error && typeof error === 'object');
assert.ok('code' in error);
assert.deepStrictEqual((error as unknown as Record<PropertyKey, unknown>)['code'], ErrorCode.AppInitializationError);
assert(authorizeCallback.notCalled);
}
});
Expand All @@ -171,7 +188,9 @@ describe('App basic features', () => {
new MockApp({ authorize: noop });
assert.fail();
} catch (error) {
assert.propertyVal(error, 'code', ErrorCode.AppInitializationError);
assert.ok(error && typeof error === 'object');
assert.ok('code' in error);
assert.deepStrictEqual((error as unknown as Record<PropertyKey, unknown>)['code'], ErrorCode.AppInitializationError);
}
});
it('should fail when both socketMode and a custom receiver are specified', async () => {
Expand All @@ -181,7 +200,9 @@ describe('App basic features', () => {
new MockApp({ token: '', signingSecret: '', socketMode: true, receiver: fakeReceiver });
assert.fail();
} catch (error) {
assert.propertyVal(error, 'code', ErrorCode.AppInitializationError);
assert.ok(error && typeof error === 'object');
assert.ok('code' in error);
assert.deepStrictEqual((error as unknown as Record<PropertyKey, unknown>)['code'], ErrorCode.AppInitializationError);
}
});
it('should succeed when both socketMode and SocketModeReceiver are specified', async () => {
Expand Down Expand Up @@ -220,7 +241,7 @@ describe('App basic features', () => {
const dummyConvoStore = createFakeConversationStore();
const MockApp = importApp(overrides);
const app = new MockApp({ convoStore: dummyConvoStore, authorize: noop, signingSecret: '' });
assert.instanceOf(app, MockApp);
assert.ok(app instanceof MockApp);
assert(fakeConversationContext.firstCall.calledWith(dummyConvoStore));
});
});
Expand All @@ -231,7 +252,9 @@ describe('App basic features', () => {
new MockApp({ token: '', signingSecret: '', redirectUri: 'http://example.com/redirect' }); // eslint-disable-line no-new
assert.fail();
} catch (error) {
assert.propertyVal(error, 'code', ErrorCode.AppInitializationError);
assert.ok(error && typeof error === 'object');
assert.ok('code' in error);
assert.deepStrictEqual((error as unknown as Record<PropertyKey, unknown>)['code'], ErrorCode.AppInitializationError);
}
});
it('should fail when missing installerOptions.redirectUriPath', async () => {
Expand All @@ -245,7 +268,9 @@ describe('App basic features', () => {
});
assert.fail();
} catch (error) {
assert.propertyVal(error, 'code', ErrorCode.AppInitializationError);
assert.ok(error && typeof error === 'object');
assert.ok('code' in error);
assert.deepStrictEqual((error as unknown as Record<PropertyKey, unknown>)['code'], ErrorCode.AppInitializationError);
}
});
});
Expand Down Expand Up @@ -306,23 +331,23 @@ describe('App basic features', () => {
signingSecret: 'invalid-one',
deferInitialization: true,
});
assert.instanceOf(app, MockApp);
assert.ok(app instanceof MockApp);
try {
await app.start();
assert.fail('The start() method should fail before init() call');
} catch (err) {
assert.propertyVal(
err,
'message',
'This App instance is not yet initialized. Call `await App#init()` before starting the app.',
);
assert.ok(err && typeof err === 'object');
assert.ok('message' in err);
assert.deepStrictEqual((err as unknown as Record<PropertyKey, unknown>)['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<PropertyKey, unknown>)['message'], exception);
}
});
});
Expand All @@ -336,8 +361,12 @@ describe('App basic features', () => {
const fakeLogger = createFakeLogger();
const MockApp = importApp(overrides);
const app = new MockApp({ logger: fakeLogger, token: '', appToken: fakeAppToken, developerMode: true });
assert.propertyVal(app, 'logLevel', LogLevel.DEBUG);
assert.propertyVal(app, 'socketMode', true);
assert.ok(app && typeof app === 'object');
assert.ok('logLevel' in app);
assert.deepStrictEqual((app as unknown as Record<PropertyKey, unknown>)['logLevel'], LogLevel.DEBUG);
assert.ok(app && typeof app === 'object');
assert.ok('socketMode' in app);
assert.deepStrictEqual((app as unknown as Record<PropertyKey, unknown>)['socketMode'], true);
});
});

Expand Down
Loading
Loading