From 3182d5a75804a57011bf94c503db0eb5a5a49997 Mon Sep 17 00:00:00 2001 From: Filip Maj Date: Wed, 4 Sep 2024 15:12:14 -0400 Subject: [PATCH] biomelint webhook --- packages/webhook/.eslintignore | 1 - packages/webhook/.eslintrc.js | 1 - packages/webhook/.gitignore | 1 - packages/webhook/.vscode/launch.json | 38 ++--- packages/webhook/.vscode/settings.json | 2 +- packages/webhook/api-extractor.json | 34 ----- packages/webhook/biome.json | 4 + packages/webhook/package.json | 32 +---- packages/webhook/src/IncomingWebhook.spec.js | 137 ------------------- packages/webhook/src/IncomingWebhook.spec.ts | 114 +++++++++++++++ packages/webhook/src/IncomingWebhook.ts | 20 ++- packages/webhook/src/errors.ts | 6 +- packages/webhook/src/instrument.ts | 17 ++- packages/webhook/tsconfig.eslint.json | 1 - packages/webhook/tsconfig.json | 11 +- 15 files changed, 164 insertions(+), 255 deletions(-) delete mode 120000 packages/webhook/.eslintignore delete mode 120000 packages/webhook/.eslintrc.js delete mode 100644 packages/webhook/api-extractor.json create mode 100644 packages/webhook/biome.json delete mode 100644 packages/webhook/src/IncomingWebhook.spec.js create mode 100644 packages/webhook/src/IncomingWebhook.spec.ts delete mode 120000 packages/webhook/tsconfig.eslint.json diff --git a/packages/webhook/.eslintignore b/packages/webhook/.eslintignore deleted file mode 120000 index 5169d53c8..000000000 --- a/packages/webhook/.eslintignore +++ /dev/null @@ -1 +0,0 @@ -../../lint-configs/.eslintignore \ No newline at end of file diff --git a/packages/webhook/.eslintrc.js b/packages/webhook/.eslintrc.js deleted file mode 120000 index 5962fa3ca..000000000 --- a/packages/webhook/.eslintrc.js +++ /dev/null @@ -1 +0,0 @@ -../../lint-configs/.eslintrc.js \ No newline at end of file diff --git a/packages/webhook/.gitignore b/packages/webhook/.gitignore index f47692353..b024219cb 100644 --- a/packages/webhook/.gitignore +++ b/packages/webhook/.gitignore @@ -6,5 +6,4 @@ /dist # coverage -/.nyc_output /coverage diff --git a/packages/webhook/.vscode/launch.json b/packages/webhook/.vscode/launch.json index f2b586f71..cdf936528 100644 --- a/packages/webhook/.vscode/launch.json +++ b/packages/webhook/.vscode/launch.json @@ -1,25 +1,15 @@ { - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "type": "node", - "request": "launch", - "name": "Webhook Tests", - "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", - "args": [ - "--timeout", - "999999", - "--colors", - "${workspaceFolder}/src/*.spec.js" - ], - "internalConsoleOptions": "openOnSessionStart", - "skipFiles": [ - "/**" - ], - "cwd": "${workspaceFolder}" - }, - ] -} \ No newline at end of file + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Webhook Tests", + "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", + "args": ["--timeout", "999999", "--colors", "${workspaceFolder}/src/*.spec.js"], + "internalConsoleOptions": "openOnSessionStart", + "skipFiles": ["/**"], + "cwd": "${workspaceFolder}" + } + ] +} diff --git a/packages/webhook/.vscode/settings.json b/packages/webhook/.vscode/settings.json index 6de18e4ed..97bef4c57 100644 --- a/packages/webhook/.vscode/settings.json +++ b/packages/webhook/.vscode/settings.json @@ -1,3 +1,3 @@ { - "typescript.tsdk": "./node_modules/typescript/lib" + "typescript.tsdk": "./node_modules/typescript/lib" } diff --git a/packages/webhook/api-extractor.json b/packages/webhook/api-extractor.json deleted file mode 100644 index dbbbba8b5..000000000 --- a/packages/webhook/api-extractor.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - /** - * Config file for API Extractor. For more info, please visit: https://api-extractor.com - */ - "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", - - "mainEntryPointFilePath": "/dist/index.d.ts", - - "apiReport": { - "enabled": false - }, - - "docModel": { - "enabled": true, - "apiJsonFilePath": "/../../support/ref-docs/models/.api.json" - }, - - "dtsRollup": { - "enabled": false - }, - - "tsdocMetadata": { - "enabled": false - }, - - "messages": { - "extractorMessageReporting": { - // Disable release tagging warnings - "ae-missing-release-tag": { - "logLevel": "none" - } - } - } -} diff --git a/packages/webhook/biome.json b/packages/webhook/biome.json new file mode 100644 index 000000000..ea710a856 --- /dev/null +++ b/packages/webhook/biome.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.8.3/schema.json", + "extends": ["../../biome.json"] +} diff --git a/packages/webhook/package.json b/packages/webhook/package.json index 716e07d2c..1984439e5 100644 --- a/packages/webhook/package.json +++ b/packages/webhook/package.json @@ -4,19 +4,10 @@ "description": "Official library for using the Slack Platform's Incoming Webhooks", "author": "Slack Technologies, LLC", "license": "MIT", - "keywords": [ - "slack", - "request", - "client", - "http", - "api", - "proxy" - ], + "keywords": ["slack", "request", "client", "http", "api", "proxy"], "main": "dist/index.js", "types": "./dist/index.d.ts", - "files": [ - "dist/**/*" - ], + "files": ["dist/**/*"], "engines": { "node": ">= 18", "npm": ">= 8.6.0" @@ -33,11 +24,10 @@ "prepare": "npm run build", "build": "npm run build:clean && tsc", "build:clean": "shx rm -rf ./dist ./coverage", - "lint": "eslint --fix --ext .ts src", - "mocha": "mocha --config .mocharc.json src/*.spec.js", + "lint": "npx @biomejs/biome check --write .", + "mocha": "mocha --config .mocharc.json src/*.spec.ts", "test": "npm run lint && npm run test:unit", - "test:unit": "npm run build && c8 npm run mocha", - "ref-docs:model": "api-extractor run" + "test:unit": "npm run build && c8 npm run mocha" }, "dependencies": { "@slack/types": "^2.9.0", @@ -45,24 +35,14 @@ "axios": "^1.7.4" }, "devDependencies": { - "@microsoft/api-extractor": "^7.38.0", + "@biomejs/biome": "^1.8.3", "@types/chai": "^4.3.5", "@types/mocha": "^10.0.1", - "@typescript-eslint/eslint-plugin": "^6.4.1", - "@typescript-eslint/parser": "^6.4.0", "c8": "^9.1.0", "chai": "^4.3.8", - "eslint": "^8.47.0", - "eslint-config-airbnb-base": "^15.0.0", - "eslint-config-airbnb-typescript": "^17.1.0", - "eslint-plugin-import": "^2.28.1", - "eslint-plugin-import-newlines": "^1.3.4", - "eslint-plugin-jsdoc": "^46.5.0", - "eslint-plugin-node": "^11.1.0", "mocha": "^10.2.0", "nock": "^13.3.3", "shx": "^0.3.2", - "sinon": "^17.0.0", "source-map-support": "^0.5.21", "ts-node": "^8.2.0", "typescript": "^4.1.0" diff --git a/packages/webhook/src/IncomingWebhook.spec.js b/packages/webhook/src/IncomingWebhook.spec.js deleted file mode 100644 index d1bc42cd9..000000000 --- a/packages/webhook/src/IncomingWebhook.spec.js +++ /dev/null @@ -1,137 +0,0 @@ -require('mocha'); -const { Agent } = require('https'); -const { IncomingWebhook } = require('./IncomingWebhook'); -const { ErrorCode } = require('./errors'); -const { assert } = require('chai'); -const nock = require('nock'); -const sinon = require('sinon'); - -const url = 'https://hooks.slack.com/services/FAKEWEBHOOK'; - -describe('IncomingWebhook', function () { - - describe('constructor()', function () { - it('should build a default webhook given a URL', function () { - const webhook = new IncomingWebhook(url); - assert.instanceOf(webhook, IncomingWebhook); - }); - - it('should create a default webhook with a default timeout', function () { - const webhook = new IncomingWebhook(url); - assert.equal(webhook.defaults.timeout, 0); - }); - - it('should create an axios instance that has the timeout passed by the user', function () { - const givenTimeout = 100; - const webhook = new IncomingWebhook(url, { timeout: givenTimeout }) - assert.equal(webhook.axios.defaults.timeout, givenTimeout) - }) - }); - - describe('send()', function () { - beforeEach(function () { - this.webhook = new IncomingWebhook(url); - }); - - describe('when making a successful call', function () { - beforeEach(function () { - this.scope = nock('https://hooks.slack.com') - .post(/services/) - .reply(200, 'ok'); - }); - - it('should return results in a Promise', function () { - const result = this.webhook.send('Hello'); - return result.then((result) => { - assert.strictEqual(result.text, 'ok'); - this.scope.done(); - }); - }); - - it('should send metadata', function () { - const result = this.webhook.send({ - text: 'Hello', - response_type: 'in_channel', - metadata: { - event_type: 'foo', - event_payload: { foo: 'bar'}, - } - }); - return result.then((result) => { - assert.strictEqual(result.text, 'ok'); - this.scope.done(); - }); - }); - }); - - describe('when the call fails', function () { - beforeEach(function () { - this.statusCode = 500; - this.scope = nock('https://hooks.slack.com') - .post(/services/) - .reply(this.statusCode); - }); - - it('should return a Promise which rejects on error', function () { - const result = this.webhook.send('Hello'); - return result.catch((error) => { - assert.ok(error); - assert.instanceOf(error, Error); - assert.match(error.message, new RegExp(this.statusCode)); - this.scope.done(); - }); - }); - - it('should fail with IncomingWebhookRequestError when the API request fails', function () { - // One known request error is when the node encounters an ECONNREFUSED. In order to simulate this, rather than - // using nock, we send the request to a host:port that is not listening. - const webhook = new IncomingWebhook('https://localhost:8999/api/'); - const result = webhook.send('Hello'); - return result.catch((error) => { - assert.instanceOf(error, Error); - assert.equal(error.code, ErrorCode.RequestError); - assert.instanceOf(error.original, Error); - }); - }); - }); - - describe('lifecycle', function () { - it('should not overwrite the default parameters after a call', function () { - const defaultParams = { channel: 'default' }; - const expectedParams = Object.assign({}, defaultParams); - const webhook = new IncomingWebhook(url, defaultParams); - - const result = webhook.send({ channel: 'different' }); - return result.catch((error) => { - assert.deepEqual(webhook.defaults, expectedParams); - }); - }); - }); - }); - - describe('has an option to set a custom HTTP agent', function () { - it('should send a request using the custom agent', function () { - const agent = new Agent({ keepAlive: true }); - const spy = sinon.spy(agent, 'addRequest'); - const webhook = new IncomingWebhook(url, { agent }); - - return webhook.send('Hello') - .catch(() => { - assert(spy.called); - }) - .then(() => { - agent.addRequest.restore(); - agent.destroy(); - }) - .catch((error) => { - agent.addRequest.restore(); - agent.destroy(); - throw error; - }); - }); - }); - - afterEach(function () { - nock.cleanAll(); - }); -}); diff --git a/packages/webhook/src/IncomingWebhook.spec.ts b/packages/webhook/src/IncomingWebhook.spec.ts new file mode 100644 index 000000000..963da77af --- /dev/null +++ b/packages/webhook/src/IncomingWebhook.spec.ts @@ -0,0 +1,114 @@ +import { assert } from 'chai'; +import nock from 'nock'; +import { IncomingWebhook } from './IncomingWebhook'; +import { ErrorCode } from './errors'; + +const url = 'https://hooks.slack.com/services/FAKEWEBHOOK'; + +describe('IncomingWebhook', () => { + afterEach(() => { + nock.cleanAll(); + }); + + describe('constructor()', () => { + it('should build a default webhook given a URL', () => { + const webhook = new IncomingWebhook(url); + assert.instanceOf(webhook, IncomingWebhook); + }); + + it('should create a default webhook with a default timeout', () => { + const webhook = new IncomingWebhook(url); + assert.nestedPropertyVal(webhook, 'defaults.timeout', 0); + }); + + it('should create an axios instance that has the timeout passed by the user', () => { + const givenTimeout = 100; + const webhook = new IncomingWebhook(url, { timeout: givenTimeout }); + assert.nestedPropertyVal(webhook, 'axios.defaults.timeout', givenTimeout); + }); + }); + + describe('send()', () => { + let webhook: IncomingWebhook; + beforeEach(() => { + webhook = new IncomingWebhook(url); + }); + + describe('when making a successful call', () => { + let scope: nock.Scope; + beforeEach(() => { + scope = nock('https://hooks.slack.com') + .post(/services/) + .reply(200, 'ok'); + }); + + it('should return results in a Promise', async () => { + const result = await webhook.send('Hello'); + assert.strictEqual(result.text, 'ok'); + scope.done(); + }); + + it('should send metadata', async () => { + const result = await webhook.send({ + text: 'Hello', + metadata: { + event_type: 'foo', + event_payload: { foo: 'bar' }, + }, + }); + assert.strictEqual(result.text, 'ok'); + scope.done(); + }); + }); + + describe('when the call fails', () => { + let statusCode: number; + let scope: nock.Scope; + beforeEach(() => { + statusCode = 500; + scope = nock('https://hooks.slack.com') + .post(/services/) + .reply(statusCode); + }); + + it('should return a Promise which rejects on error', async () => { + try { + await webhook.send('Hello'); + assert.fail('expected rejection'); + } catch (error) { + assert.ok(error); + assert.instanceOf(error, Error); + assert.match((error as Error).message, new RegExp(String(statusCode))); + scope.done(); + } + }); + + it('should fail with IncomingWebhookRequestError when the API request fails', async () => { + // One known request error is when the node encounters an ECONNREFUSED. In order to simulate this, rather than + // using nock, we send the request to a host:port that is not listening. + const webhook = new IncomingWebhook('https://localhost:8999/api/'); + try { + await webhook.send('Hello'); + assert.fail('expected rejection'); + } catch (error) { + assert.instanceOf(error, Error); + assert.propertyVal(error, 'code', ErrorCode.RequestError); + } + }); + }); + + describe('lifecycle', () => { + it('should not overwrite the default parameters after a call', async () => { + const defaultParams = { channel: 'default' }; + const webhook = new IncomingWebhook(url, defaultParams); + + try { + await webhook.send({ channel: 'different' }); + assert.fail('expected rejection'); + } catch (error) { + assert.nestedPropertyVal(webhook, 'defaults.channel', defaultParams.channel); + } + }); + }); + }); +}); diff --git a/packages/webhook/src/IncomingWebhook.ts b/packages/webhook/src/IncomingWebhook.ts index ba2aa3ab0..8b5046121 100644 --- a/packages/webhook/src/IncomingWebhook.ts +++ b/packages/webhook/src/IncomingWebhook.ts @@ -1,7 +1,7 @@ -import { Agent } from 'http'; +import type { Agent } from 'node:http'; -import { Block, KnownBlock, MessageAttachment } from '@slack/types'; // TODO: Block and KnownBlock will be merged into AnyBlock in upcoming types release -import axios, { AxiosInstance, AxiosResponse } from 'axios'; +import type { Block, KnownBlock, MessageAttachment } from '@slack/types'; // TODO: Block and KnownBlock will be merged into AnyBlock in upcoming types release +import axios, { type AxiosInstance, type AxiosResponse } from 'axios'; import { httpErrorWithOriginal, requestErrorWithOriginal } from './errors'; import { getUserAgent } from './instrument'; @@ -46,12 +46,11 @@ export class IncomingWebhook { proxy: false, timeout: defaults.timeout, headers: { - // eslint-disable-next-line @typescript-eslint/naming-convention 'User-Agent': getUserAgent(), }, }); - delete this.defaults.agent; + this.defaults.agent = undefined; } /** @@ -71,23 +70,22 @@ export class IncomingWebhook { try { const response = await this.axios.post(this.url, payload); return this.buildResult(response); - // eslint-disable-next-line @typescript-eslint/no-explicit-any + // biome-ignore lint/suspicious/noExplicitAny: errors can be anything } catch (error: any) { // Wrap errors in this packages own error types (abstract the implementation details' types) if (error.response !== undefined) { throw httpErrorWithOriginal(error); - } else if (error.request !== undefined) { + } + if (error.request !== undefined) { throw requestErrorWithOriginal(error); - } else { - throw error; } + throw error; } } /** * Processes an HTTP response into an IncomingWebhookResult. */ - // eslint-disable-next-line class-methods-use-this private buildResult(response: AxiosResponse): IncomingWebhookResult { return { text: response.data, @@ -117,7 +115,7 @@ export interface IncomingWebhookSendArguments extends IncomingWebhookDefaultArgu unfurl_media?: boolean; metadata?: { event_type: string; - // eslint-disable-next-line @typescript-eslint/no-explicit-any + // biome-ignore lint/suspicious/noExplicitAny: errors can be anything event_payload: Record; }; } diff --git a/packages/webhook/src/errors.ts b/packages/webhook/src/errors.ts index f209565ba..1252b190a 100644 --- a/packages/webhook/src/errors.ts +++ b/packages/webhook/src/errors.ts @@ -1,4 +1,4 @@ -import { AxiosError, AxiosResponse } from 'axios'; +import type { AxiosError, AxiosResponse } from 'axios'; /** * All errors produced by this package adhere to this interface @@ -47,7 +47,7 @@ export function requestErrorWithOriginal(original: AxiosError): IncomingWebhookR ErrorCode.RequestError, ) as Partial; error.original = original; - return (error as IncomingWebhookRequestError); + return error as IncomingWebhookRequestError; } /** @@ -60,5 +60,5 @@ export function httpErrorWithOriginal(original: AxiosError & { response: AxiosRe ErrorCode.HTTPError, ) as Partial; error.original = original; - return (error as IncomingWebhookHTTPError); + return error as IncomingWebhookHTTPError; } diff --git a/packages/webhook/src/instrument.ts b/packages/webhook/src/instrument.ts index 9cbc08051..494acda81 100644 --- a/packages/webhook/src/instrument.ts +++ b/packages/webhook/src/instrument.ts @@ -1,4 +1,4 @@ -import * as os from 'os'; +import * as os from 'node:os'; const packageJson = require('../package.json'); // eslint-disable-line import/no-commonjs, @typescript-eslint/no-var-requires @@ -9,9 +9,10 @@ function replaceSlashes(s: string): string { return s.replace('/', ':'); } -const baseUserAgent = `${replaceSlashes(packageJson.name)}/${packageJson.version} ` + - `node/${process.version.replace('v', '')} ` + - `${os.platform()}/${os.release()}`; +const baseUserAgent = + `${replaceSlashes(packageJson.name)}/${packageJson.version} ` + + `node/${process.version.replace('v', '')} ` + + `${os.platform()}/${os.release()}`; const appMetadata: { [key: string]: string } = {}; @@ -20,7 +21,7 @@ const appMetadata: { [key: string]: string } = {}; * @param appMetadata.name name of tool to be counted in instrumentation * @param appMetadata.version version of tool to be counted in instrumentation */ -export function addAppMetadata({ name, version }: { name: string, version: string }): void { +export function addAppMetadata({ name, version }: { name: string; version: string }): void { appMetadata[replaceSlashes(name)] = version; } @@ -28,7 +29,9 @@ export function addAppMetadata({ name, version }: { name: string, version: strin * Returns the current User-Agent value for instrumentation */ export function getUserAgent(): string { - const appIdentifier = Object.entries(appMetadata).map(([name, version]) => `${name}/${version}`).join(' '); + const appIdentifier = Object.entries(appMetadata) + .map(([name, version]) => `${name}/${version}`) + .join(' '); // only prepend the appIdentifier when its not empty - return ((appIdentifier.length > 0) ? `${appIdentifier} ` : '') + baseUserAgent; + return (appIdentifier.length > 0 ? `${appIdentifier} ` : '') + baseUserAgent; } diff --git a/packages/webhook/tsconfig.eslint.json b/packages/webhook/tsconfig.eslint.json deleted file mode 120000 index 2cd9fb812..000000000 --- a/packages/webhook/tsconfig.eslint.json +++ /dev/null @@ -1 +0,0 @@ -../../lint-configs/tsconfig.eslint.json \ No newline at end of file diff --git a/packages/webhook/tsconfig.json b/packages/webhook/tsconfig.json index b61d86ce5..4707b9610 100644 --- a/packages/webhook/tsconfig.json +++ b/packages/webhook/tsconfig.json @@ -19,20 +19,15 @@ "paths": { "*": ["./types/*"] }, - "esModuleInterop" : true, + "esModuleInterop": true // Not using this setting because its only used to require the package.json file, and that would change the // structure of the files in the dist directory because package.json is not located inside src. It would be nice // to use import instead of require(), but its not worth the tradeoff of restructuring the build (for now). // "resolveJsonModule": true, }, - "include": [ - "src/**/*" - ], - "exclude": [ - "src/**/*.spec.js", - "src/**/*.js" - ], + "include": ["src/**/*"], + "exclude": ["src/**/*.spec.js", "src/**/*.js"], "jsdoc": { "out": "support/jsdoc", "access": "public"