diff --git a/package-lock.json b/package-lock.json index 414c9325..97c1895a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,8 +13,7 @@ "@actions/github": "^9.1.1", "@slack/logger": "^4.0.1", "@slack/web-api": "^7.16.0", - "axios": "^1.18.1", - "axios-retry": "^4.5.0", + "@slack/webhook": "7.2.0-rc.2", "flat": "^6.0.1", "https-proxy-agent": "^9.0.0", "js-yaml": "^4.2.0", @@ -108,9 +107,9 @@ } }, "node_modules/@biomejs/biome": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.5.1.tgz", - "integrity": "sha512-IXWLCxKmae+rI7LOHS1B3EbVisQ6GRAWbhN9msa6KjNCyFWrvKZWR4oUdinaNssrV852OrSHuSPa95h1GPJc7Q==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.5.2.tgz", + "integrity": "sha512-VQ3RCqr7JmDIX+w6stWYl+g/3bYofN3q2wDBHUKKc/c7i5QWrFKFBZYCYPWTE6agsUPMIZZe6/CMmVUfUAhkKA==", "dev": true, "license": "MIT OR Apache-2.0", "bin": { @@ -124,20 +123,20 @@ "url": "https://opencollective.com/biome" }, "optionalDependencies": { - "@biomejs/cli-darwin-arm64": "2.5.1", - "@biomejs/cli-darwin-x64": "2.5.1", - "@biomejs/cli-linux-arm64": "2.5.1", - "@biomejs/cli-linux-arm64-musl": "2.5.1", - "@biomejs/cli-linux-x64": "2.5.1", - "@biomejs/cli-linux-x64-musl": "2.5.1", - "@biomejs/cli-win32-arm64": "2.5.1", - "@biomejs/cli-win32-x64": "2.5.1" + "@biomejs/cli-darwin-arm64": "2.5.2", + "@biomejs/cli-darwin-x64": "2.5.2", + "@biomejs/cli-linux-arm64": "2.5.2", + "@biomejs/cli-linux-arm64-musl": "2.5.2", + "@biomejs/cli-linux-x64": "2.5.2", + "@biomejs/cli-linux-x64-musl": "2.5.2", + "@biomejs/cli-win32-arm64": "2.5.2", + "@biomejs/cli-win32-x64": "2.5.2" } }, "node_modules/@biomejs/cli-darwin-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.5.1.tgz", - "integrity": "sha512-npqDzvqv7vFaWRiNN1Te71siRgPaqS9MpqgYCdP/CrUbkJ7ApezaeaKjueKHRN/JH/6lRjJQAHi8acQDCAz22w==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.5.2.tgz", + "integrity": "sha512-e7P3P7EkwFc/KiX2AHw4YDLIBOMfG9CPCAwy52k5Bp0dfhkozx9hf6wCmIr2QeXy2XeccJ3V/Sg+hDmzYEqxSg==", "cpu": [ "arm64" ], @@ -152,9 +151,9 @@ } }, "node_modules/@biomejs/cli-darwin-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.5.1.tgz", - "integrity": "sha512-RgwTqPAM8g2tn1j+b5oRjF/DbSBX8a4gwojtuG9XuhfK7GgomvZ9+T+tqjXiVbjLEeGJOoL6VEk8mvRTVeSybw==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.5.2.tgz", + "integrity": "sha512-ymzMvjC1Jg0b9K0D26ZdARqFQXs7MocfLC5FOCGfkC0Ss+ACUJkX5364ZM5nT4NLZanHRZNVrZEy+Ibwcvux/g==", "cpu": [ "x64" ], @@ -169,9 +168,9 @@ } }, "node_modules/@biomejs/cli-linux-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.5.1.tgz", - "integrity": "sha512-yhV35CzZh38VyMvTEXi3JTjxZBs++oCKK9KG8vB6VI5+uvQvZNR3BFWEKKzuOmx9DJJj7sQpZ4LQJcmbGTs3+Q==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.5.2.tgz", + "integrity": "sha512-t7sseOmqND57uUWTwlawU6BYj+J06T/9EkydzBhkrgw/FK3QVhjU2wsJR0frljrKZ0/I8A/rYw7284QgqjQfIQ==", "cpu": [ "arm64" ], @@ -189,9 +188,9 @@ } }, "node_modules/@biomejs/cli-linux-arm64-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.5.1.tgz", - "integrity": "sha512-WMcvMLgByyTqVxGlq918NBBYliq9FRR9GAQVETHb+VjGVqXCZFfHlZHC1FX4ibuYY/Hg6TJE3rHU0xVrdJXNRw==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.5.2.tgz", + "integrity": "sha512-w+ANG0ZvTu9IeEg9QnstoOnk6L0fpwJifW6aHR18+cb5Z39bkANItYjAfMrnvce5tmMK+IQ6nPX7/kQFdam5iw==", "cpu": [ "arm64" ], @@ -209,9 +208,9 @@ } }, "node_modules/@biomejs/cli-linux-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.5.1.tgz", - "integrity": "sha512-J/7uHSX7NfoYDI7HijAkd8lnQIOrRb2W7j3X+tw4R+N5ExvXGsyXFiGdQcfcxfOmNQmZVSQOCDk757fwpzqQcg==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.5.2.tgz", + "integrity": "sha512-M/lOZrewzTCRDINbjhQ1gYYru37KlD3kJBQwwKCG0ckz5E9IZwIoJ3X0wBwRXA+yBDIwWUuPBHS67HzJY4dTfA==", "cpu": [ "x64" ], @@ -229,9 +228,9 @@ } }, "node_modules/@biomejs/cli-linux-x64-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.5.1.tgz", - "integrity": "sha512-ANTowtlLmPYm5yeMckWY8Xzb9Ix+JJP3tgHR/n6xRj1VWyIzzWtfRfih9hv9VmClwadpBvZduISZIbBsIlYG3A==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.5.2.tgz", + "integrity": "sha512-VArNLAzND063tF+XY0yPyM+DyahpzOMzOAvb7qs259nhjJWRjvjZdssuA+Rfl+l07+NOesKZ0Xu2yFrXyBMtzw==", "cpu": [ "x64" ], @@ -249,9 +248,9 @@ } }, "node_modules/@biomejs/cli-win32-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.5.1.tgz", - "integrity": "sha512-zgXnKNgWPC4iPF7Y1lR3STUeCUuZRpD6IiOrC7TZTlh0Lx6FiVUT05myuMQHQ9D+1cc7uyMldi4forE6lp0ivQ==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.5.2.tgz", + "integrity": "sha512-kbjFFKyZlzYnAuw7sRy5qDoFG6zrP40UK08oPQsWK0ct3NMnGSt+Bs1iviEEyEIP57N5MrykGXdO/wRiaR4lww==", "cpu": [ "arm64" ], @@ -266,9 +265,9 @@ } }, "node_modules/@biomejs/cli-win32-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.5.1.tgz", - "integrity": "sha512-6uxpR9hvaglANkZemeSiN/FhYgkGasrEGn267eXIWvjrjJ2LhDlk251IhjVJq6MXzkV2/bcXwLwSroLyPtqRZg==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.5.2.tgz", + "integrity": "sha512-4InchVpdVmdkkkgjQqKpgvyu+VPnoF/7RPSw5YATgEVpt2j72wcCAeV5TwaE9ZGJUZWZn7v2CwSAj6CrMJEx8A==", "cpu": [ "x64" ], @@ -871,6 +870,50 @@ "npm": ">= 8.6.0" } }, + "node_modules/@slack/web-api/node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "license": "MIT", + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@slack/webhook": { + "version": "7.2.0-rc.2", + "resolved": "https://registry.npmjs.org/@slack/webhook/-/webhook-7.2.0-rc.2.tgz", + "integrity": "sha512-4O0LKiIFJdHBqOPb6lkO9SxqRN9RVIWXMI9QBPzfZrDiLC3pJACQNCETauLPbMonGtJYrt8B/xOOEQA+GCuWfQ==", + "license": "MIT", + "dependencies": { + "@slack/types": "^2.20.1", + "@types/node": ">=18", + "@types/retry": "0.12.0", + "axios": "^1.16.0", + "p-retry": "^4.6.2", + "retry": "^0.13.1" + }, + "engines": { + "node": ">= 18", + "npm": ">= 8.6.0" + } + }, + "node_modules/@slack/webhook/node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "license": "MIT", + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/@types/flat": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/@types/flat/-/flat-5.0.5.tgz", @@ -985,54 +1028,16 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "node_modules/axios": { - "version": "1.18.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.1.tgz", - "integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.0.tgz", + "integrity": "sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==", "license": "MIT", "dependencies": { "follow-redirects": "^1.16.0", "form-data": "^4.0.5", - "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } }, - "node_modules/axios-retry": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/axios-retry/-/axios-retry-4.5.0.tgz", - "integrity": "sha512-aR99oXhpEDGo0UuAlYcn2iGRds30k366Zfa05XWScR9QaQD4JYiP3/1Qt1u7YlefUOK+cn0CcwoL1oefavQUlQ==", - "license": "Apache-2.0", - "dependencies": { - "is-retry-allowed": "^2.2.0" - }, - "peerDependencies": { - "axios": "0.x || 1.x" - } - }, - "node_modules/axios/node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "license": "MIT", - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/axios/node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "license": "MIT", - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/before-after-hook": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz", @@ -1638,18 +1643,6 @@ "node": ">=0.12.0" } }, - "node_modules/is-retry-allowed": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-2.2.0.tgz", - "integrity": "sha512-XVm7LOeLpTW4jV19QSH38vkswxoLud8sQ57YwJVTPWdiaI9I8keEhGFpBlslyVsgdQy4Opg8QOLb8YRgsyZiQg==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -1909,19 +1902,6 @@ "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", "license": "MIT" }, - "node_modules/p-retry": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", - "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", - "license": "MIT", - "dependencies": { - "@types/retry": "0.12.0", - "retry": "^0.13.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/p-timeout": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", diff --git a/package.json b/package.json index 1faca836..d4edfce4 100644 --- a/package.json +++ b/package.json @@ -46,8 +46,7 @@ "@actions/github": "^9.1.1", "@slack/logger": "^4.0.1", "@slack/web-api": "^7.16.0", - "axios": "^1.18.1", - "axios-retry": "^4.5.0", + "@slack/webhook": "7.2.0-rc.2", "flat": "^6.0.1", "https-proxy-agent": "^9.0.0", "js-yaml": "^4.2.0", diff --git a/src/config.js b/src/config.js index 9febb986..df59cce6 100644 --- a/src/config.js +++ b/src/config.js @@ -1,6 +1,5 @@ -import os from "node:os"; import webapi from "@slack/web-api"; -import axios from "axios"; +import webhook from "@slack/webhook"; import packageJson from "../package.json" with { type: "json" }; import Content from "./content.js"; import SlackError from "./errors.js"; @@ -60,11 +59,6 @@ export default class Config { */ inputs; - /** - * @type {import("axios").AxiosStatic} - The axios client. - */ - axios; - /** * @type {Content} - The parsed payload data to send. */ @@ -87,6 +81,11 @@ export default class Config { */ webapi; + /** + * @type {import("@slack/webhook")} - Slack webhook client. + */ + webhook; + /** * Gather values from the job inputs and use defaults or error for the missing * ones. @@ -98,10 +97,10 @@ export default class Config { * @param {import("@actions/core")} core - GitHub Actions core utilities. */ constructor(core) { - this.axios = axios; this.core = core; this.logger = new Logger(core).logger; this.webapi = webapi; + this.webhook = webhook; this.inputs = { api: core.getInput("api"), errors: core.getBooleanInput("errors"), @@ -137,11 +136,10 @@ export default class Config { name: packageJson.name, version: packageJson.version, }); - this.axios.defaults.headers.common["User-Agent"] = - `${packageJson.name.replace("/", ":")}/${packageJson.version} ` + - `axios/${this.axios.VERSION} ` + - `node/${process.version.replace("v", "")} ` + - `${os.platform()}/${os.release()}`; + this.webhook.addAppMetadata({ + name: packageJson.name, + version: packageJson.version, + }); } /** diff --git a/src/webhook.js b/src/webhook.js index 0d6df7d4..57e0c51c 100644 --- a/src/webhook.js +++ b/src/webhook.js @@ -1,4 +1,4 @@ -import axiosRetry, { exponentialDelay, linearDelay } from "axios-retry"; +import webhook from "@slack/webhook"; import { HttpsProxyAgent } from "https-proxy-agent"; import Config from "./config.js"; import SlackError from "./errors.js"; @@ -6,6 +6,8 @@ import SlackError from "./errors.js"; /** * This Webhook class posts the configured payload to the provided webhook, with * whatever additional settings set. + * + * @see {@link https://docs.slack.dev/tools/node-slack-sdk/webhook/} */ export default class Webhook { /** @@ -15,37 +17,53 @@ export default class Webhook { if (!config.inputs.webhook) { throw new SlackError(config.core, "No webhook was provided to post to"); } - /** - * @type {import("axios-retry").IAxiosRetryConfig} - * @see {@link https://www.npmjs.com/package/axios-retry} - */ - const retries = this.retries(config.inputs.retries); - axiosRetry(config.axios, retries); + const url = config.inputs.webhook; + const options = { + agent: this.proxies(config), + retryConfig: this.retries(config.inputs.retries), + }; try { - const response = await config.axios.post( - config.inputs.webhook, - config.content.values, - { - ...this.proxies(config), - }, - ); - config.core.setOutput("ok", response.status === 200); - config.core.setOutput("response", JSON.stringify(response.data)); - config.core.debug(JSON.stringify(response.data)); + switch (config.inputs.webhookType) { + case "incoming-webhook": { + const response = await new config.webhook.IncomingWebhook( + url, + options, + ).send(config.content.values); + config.core.setOutput("ok", true); + config.core.setOutput("response", JSON.stringify(response.text)); + config.core.debug(JSON.stringify(response.text)); + return; + } + case "webhook-trigger": { + const response = await new config.webhook.WebhookTrigger( + url, + options, + ).send(config.content.values); + config.core.setOutput("ok", response.ok); + config.core.setOutput("response", JSON.stringify(response.body)); + config.core.debug(JSON.stringify(response.body)); + return; + } + default: + throw new SlackError( + config.core, + `Unknown webhook type: ${config.inputs.webhookType}`, + ); + } } catch (/** @type {any} */ err) { - const response = err.toJSON(); - config.core.setOutput("ok", response.status === 200); - config.core.setOutput("response", JSON.stringify(response.message)); - config.core.debug(response); - throw new SlackError(config.core, response.message); + config.core.setOutput("ok", false); + config.core.setOutput("response", JSON.stringify(err.message)); + config.core.debug(err); + throw new SlackError(config.core, err.message); } } /** * Return configurations for http proxy options if these are set. * @param {Config} config - * @returns {import("axios").AxiosRequestConfig | undefined} + * @returns {HttpsProxyAgent | undefined} * @see {@link https://github.com/slackapi/slack-github-action/pull/132} + * @see {@link https://github.com/slackapi/slack-github-action/pull/205} */ proxies(config) { const { webhook, proxy } = config.inputs; @@ -64,14 +82,8 @@ export default class Webhook { } switch (new URL(proxy).protocol) { case "https:": - return { - httpsAgent: new HttpsProxyAgent(proxy), - }; case "http:": - return { - httpsAgent: new HttpsProxyAgent(proxy), - proxy: false, - }; + return new HttpsProxyAgent(proxy); default: throw new SlackError( config.core, @@ -88,36 +100,20 @@ export default class Webhook { /** * Return configurations for retry options with different delays. * @param {string} option - * @returns {import("axios-retry").IAxiosRetryConfig} + * @returns {import("@slack/webhook").RetryOptions} */ retries(option) { switch (option?.trim().toUpperCase()) { case "0": return { retries: 0 }; case "5": - return { - retryCondition: axiosRetry.isRetryableError, - retries: 5, - retryDelay: linearDelay(60 * 1000), // 5 minutes - }; + return webhook.retryPolicies.fiveRetriesInFiveMinutes; case "10": - return { - retryCondition: axiosRetry.isRetryableError, - retries: 10, - retryDelay: (count, err) => exponentialDelay(count, err, 2 * 1000), // 34.12 minutes - }; + return webhook.retryPolicies.tenRetriesInAboutThirtyMinutes; case "RAPID": - return { - retryCondition: axiosRetry.isRetryableError, - retries: 12, - retryDelay: linearDelay(1 * 1000), // 12 seconds - }; + return webhook.retryPolicies.rapidRetryPolicy; default: - return { - retryCondition: axiosRetry.isRetryableError, - retries: 5, - retryDelay: linearDelay(60 * 1000), // 5 minutes - }; + return webhook.retryPolicies.fiveRetriesInFiveMinutes; } } } diff --git a/test/client.spec.js b/test/client.spec.js index 26e613b9..c6e9a2f6 100644 --- a/test/client.spec.js +++ b/test/client.spec.js @@ -492,14 +492,14 @@ describe("client", () => { }); it('does not attempt retries when "0" is set', async () => { - const webhook = new Client(); - const result = webhook.retries("0"); + const client = new Client(); + const result = client.retries("0"); assert.equal(result.retries, 0); }); it('attempts a default amount of "5" retries', async () => { - const webhook = new Client(); - const result = webhook.retries("5"); + const client = new Client(); + const result = client.retries("5"); assert.equal( result.retries, webapi.retryPolicies.fiveRetriesInFiveMinutes.retries, @@ -511,8 +511,8 @@ describe("client", () => { }); it('attempts "10" retries in around "30" minutes', async () => { - const webhook = new Client(); - const result = webhook.retries("10"); + const client = new Client(); + const result = client.retries("10"); assert.equal( result.retries, webapi.retryPolicies.tenRetriesInAboutThirtyMinutes.retries, @@ -524,8 +524,8 @@ describe("client", () => { }); it('attempts a "rapid " burst of "12" retries in seconds', async () => { - const webhook = new Client(); - const result = webhook.retries("rapid "); + const client = new Client(); + const result = client.retries("rapid "); assert.equal( result.retries, webapi.retryPolicies.rapidRetryPolicy.retries, @@ -534,8 +534,8 @@ describe("client", () => { }); it('attempts a "RAPID" burst of "12" retries in seconds', async () => { - const webhook = new Client(); - const result = webhook.retries("RAPID"); + const client = new Client(); + const result = client.retries("RAPID"); assert.equal( result.retries, webapi.retryPolicies.rapidRetryPolicy.retries, diff --git a/test/config.spec.js b/test/config.spec.js index 17c292f3..029f7eb7 100644 --- a/test/config.spec.js +++ b/test/config.spec.js @@ -1,6 +1,7 @@ import assert from "node:assert"; import { beforeEach, describe, it } from "node:test"; import webapi from "@slack/web-api"; +import webhook from "@slack/webhook"; import sinon from "sinon"; import Config from "../src/config.js"; import SlackError from "../src/errors.js"; @@ -185,18 +186,30 @@ describe("config", () => { }); it("adds metadata to webhook with package name and version", async () => { - mocks.core.getInput.withArgs("method").returns("chat.postMessage"); - mocks.core.getInput.withArgs("token").returns("xoxb-example"); - const config = new Config(mocks.core); - assert.ok( - config.axios.defaults.headers.common["User-Agent"].startsWith( - "@slack:slack-github-action/", - ), - ); - assert.ok( - config.axios.defaults.headers.common["User-Agent"].length > - "@slack:slack-github-action/".length, + const stub = sinon.stub(); + const original = Object.getOwnPropertyDescriptor( + webhook, + "addAppMetadata", ); + Object.defineProperty(webhook, "addAppMetadata", { + value: stub, + configurable: true, + }); + try { + mocks.core.getInput + .withArgs("webhook") + .returns("https://hooks.slack.com"); + mocks.core.getInput + .withArgs("webhook-type") + .returns("incoming-webhook"); + new Config(mocks.core); + assert.ok(stub.calledOnce); + const { name, version } = stub.firstCall.args[0]; + assert.equal(name, "@slack/slack-github-action"); + assert.ok(version); + } finally { + Object.defineProperty(webhook, "addAppMetadata", original); + } }); }); @@ -225,7 +238,7 @@ describe("config", () => { describe("validate", () => { it('allow the "retries" option with lowercased space', async () => { - mocks.axios.post.returns(Promise.resolve("LGTM")); + mocks.webhook.incoming.resolves({ text: "LGTM" }); mocks.core.getInput.withArgs("retries").returns(" rapid "); mocks.core.getInput .withArgs("webhook") @@ -247,7 +260,7 @@ describe("config", () => { }); it("errors if an invalid retries option is provided", async () => { - mocks.axios.post.returns(Promise.resolve("LGTM")); + mocks.webhook.incoming.resolves({ text: "LGTM" }); mocks.core.getInput.withArgs("retries").returns("FOREVER"); mocks.core.getInput .withArgs("webhook") diff --git a/test/index.spec.js b/test/index.spec.js index 728102be..d4901f9f 100644 --- a/test/index.spec.js +++ b/test/index.spec.js @@ -1,6 +1,6 @@ import fs from "node:fs"; import webapi from "@slack/web-api"; -import axios, { AxiosError } from "axios"; +import webhook from "@slack/webhook"; import sinon from "sinon"; /** @@ -19,21 +19,6 @@ import sinon from "sinon"; * The Mock class sets expected behaviors and test listeners for dependencies. */ export class Mock { - /** - * @typedef Errors - A collection of mocked errors to use in tests. - * @prop {Object.} axios - The mocked axios errors. - */ - - /** - * The mocked errors. - * @type {Errors} - */ - errors = { - axios: { - network_failed: new AxiosError("network_failed"), - }, - }; - /** * Setup stubbed dependencies and configure default input arguments for all * tests. @@ -42,7 +27,6 @@ export class Mock { */ constructor() { this.sandbox = sinon.createSandbox(); - this.axios = this.sandbox.stub(axios); this.calls = this.sandbox.stub(webapi.WebClient.prototype, "apiCall"); this.core = { debug: this.sandbox.stub(), @@ -64,6 +48,10 @@ export class Mock { }); }, }; + this.webhook = { + incoming: this.sandbox.stub(webhook.IncomingWebhook.prototype, "send"), + trigger: this.sandbox.stub(webhook.WebhookTrigger.prototype, "send"), + }; this.core.getInput.withArgs("errors").returns("false"); this.core.getInput.withArgs("retries").returns("5"); } @@ -73,7 +61,6 @@ export class Mock { */ reset() { this.sandbox.reset(); - this.axios.post.resetHistory(); this.calls.resetHistory(); this.core.debug.reset(); this.core.error.reset(); @@ -92,6 +79,8 @@ export class Mock { }); }, }; + this.webhook.incoming.resetHistory(); + this.webhook.trigger.resetHistory(); this.core.getInput.withArgs("errors").returns("false"); this.core.getInput.withArgs("retries").returns("5"); process.env.SLACK_TOKEN = ""; diff --git a/test/send.spec.js b/test/send.spec.js index 9ab906ee..f89ab5f9 100644 --- a/test/send.spec.js +++ b/test/send.spec.js @@ -24,9 +24,7 @@ describe("send", () => { .returns("https://hooks.slack.com"); mocks.core.getInput.withArgs("webhook-type").returns("webhook-trigger"); mocks.core.getInput.withArgs("payload").returns('"greetings": "hello"'); - mocks.axios.post.returns( - Promise.resolve({ status: 200, data: { ok: true } }), - ); + mocks.webhook.trigger.resolves({ ok: true, body: { ok: true } }); await send(mocks.core); assert.equal(mocks.core.setOutput.getCall(0).firstArg, "ok"); assert.equal(mocks.core.setOutput.getCall(0).lastArg, true); @@ -64,7 +62,7 @@ describe("send", () => { .returns("https://hooks.slack.com"); mocks.core.getInput.withArgs("webhook-type").returns("incoming-webhook"); mocks.core.getInput.withArgs("payload").returns('"text": "hello"'); - mocks.axios.post.returns(Promise.resolve({ status: 200, data: "ok" })); + mocks.webhook.incoming.resolves({ text: "ok" }); await send(mocks.core); assert.equal(mocks.core.setOutput.getCall(0).firstArg, "ok"); assert.equal(mocks.core.setOutput.getCall(0).lastArg, true); diff --git a/test/webhook.spec.js b/test/webhook.spec.js index 09867e68..1c097b27 100644 --- a/test/webhook.spec.js +++ b/test/webhook.spec.js @@ -1,6 +1,6 @@ import assert from "node:assert"; import { beforeEach, describe, it } from "node:test"; -import { AxiosError } from "axios"; +import webhook from "@slack/webhook"; import Config from "../src/config.js"; import SlackError from "../src/errors.js"; import send from "../src/send.js"; @@ -19,16 +19,13 @@ describe("webhook", () => { .returns("https://hooks.slack.com"); mocks.core.getInput.withArgs("webhook-type").returns("webhook-trigger"); mocks.core.getInput.withArgs("payload").returns("drinks: coffee"); - mocks.axios.post.returns( - Promise.resolve({ status: 200, data: { ok: true } }), - ); + mocks.webhook.trigger.resolves({ ok: true, body: { ok: true } }); try { await send(mocks.core); - assert.equal(mocks.axios.post.getCalls().length, 1); - const [url, payload, options] = mocks.axios.post.getCall(0).args; - assert.equal(url, "https://hooks.slack.com"); - assert.deepEqual(payload, { drinks: "coffee" }); - assert.deepEqual(options, {}); + assert.equal(mocks.webhook.trigger.getCalls().length, 1); + assert.deepEqual(mocks.webhook.trigger.getCall(0).firstArg, { + drinks: "coffee", + }); assert.equal(mocks.core.setOutput.getCall(0).firstArg, "ok"); assert.equal(mocks.core.setOutput.getCall(0).lastArg, true); assert.equal(mocks.core.setOutput.getCall(1).firstArg, "response"); @@ -48,14 +45,13 @@ describe("webhook", () => { .returns("https://hooks.slack.com"); mocks.core.getInput.withArgs("webhook-type").returns("incoming-webhook"); mocks.core.getInput.withArgs("payload").returns("text: greetings"); - mocks.axios.post.returns(Promise.resolve({ status: 200, data: "ok" })); + mocks.webhook.incoming.resolves({ text: "ok" }); try { await send(mocks.core); - assert.equal(mocks.axios.post.getCalls().length, 1); - const [url, payload, options] = mocks.axios.post.getCall(0).args; - assert.equal(url, "https://hooks.slack.com"); - assert.deepEqual(payload, { text: "greetings" }); - assert.deepEqual(options, {}); + assert.equal(mocks.webhook.incoming.getCalls().length, 1); + assert.deepEqual(mocks.webhook.incoming.getCall(0).firstArg, { + text: "greetings", + }); assert.equal(mocks.core.setOutput.getCall(0).firstArg, "ok"); assert.equal(mocks.core.setOutput.getCall(0).lastArg, true); assert.equal(mocks.core.setOutput.getCall(1).firstArg, "response"); @@ -91,71 +87,87 @@ describe("webhook", () => { } }); + it("errors when an unknown webhook type is provided", async () => { + /** + * @type {Config} + */ + const config = { + core: mocks.core, + inputs: { + webhook: "https://hooks.slack.com", + webhookType: "unknown-webhook", + retries: "5", + }, + }; + try { + await new Webhook().post(config); + assert.fail(); + } catch (err) { + if (err instanceof SlackError) { + assert.ok( + err.message.includes("Unknown webhook type: unknown-webhook"), + ); + } else { + assert.fail(err); + } + } + }); + it("returns the failures from a webhook trigger", async () => { + mocks.core.getBooleanInput.withArgs("errors").returns(true); mocks.core.getInput .withArgs("webhook") .returns("https://hooks.slack.com"); mocks.core.getInput.withArgs("webhook-type").returns("webhook-trigger"); mocks.core.getInput.withArgs("payload").returns("drinks: coffee"); - const response = new AxiosError( - "Request failed with status code 400", - "ERR_BAD_REQUEST", - {}, - {}, - { status: 400 }, + mocks.webhook.trigger.rejects( + new Error("An HTTP protocol error occurred"), ); - mocks.axios.post.resolves(Promise.reject(response)); try { await send(mocks.core); + assert.fail(); } catch (err) { if (err instanceof SlackError) { - assert.ok( - err.message.includes("Request failed with status code 400"), - ); + assert.ok(err.message.includes("An HTTP protocol error occurred")); } else { assert.fail(err); } } - assert.equal(mocks.axios.post.getCalls().length, 1); - const [url, payload, options] = mocks.axios.post.getCall(0).args; - assert.equal(url, "https://hooks.slack.com"); - assert.deepEqual(payload, { drinks: "coffee" }); - assert.deepEqual(options, {}); + assert.equal(mocks.webhook.trigger.getCalls().length, 1); + assert.deepEqual(mocks.webhook.trigger.getCall(0).firstArg, { + drinks: "coffee", + }); + assert.ok(mocks.core.setFailed.called); assert.equal(mocks.core.setOutput.getCall(0).firstArg, "ok"); assert.equal(mocks.core.setOutput.getCall(0).lastArg, false); assert.equal(mocks.core.setOutput.getCall(1).firstArg, "response"); }); it("returns the failures from an incoming webhook", async () => { + mocks.core.getBooleanInput.withArgs("errors").returns(true); mocks.core.getInput .withArgs("webhook") .returns("https://hooks.slack.com"); mocks.core.getInput.withArgs("webhook-type").returns("incoming-webhook"); mocks.core.getInput.withArgs("payload").returns("textt: oops"); - const response = new AxiosError( - "Request failed with status code 400", - "ERR_BAD_REQUEST", - {}, - {}, - { status: 400 }, + mocks.webhook.incoming.rejects( + new Error("An HTTP protocol error occurred"), ); - mocks.axios.post.resolves(Promise.reject(response)); try { await send(mocks.core); + assert.fail(); } catch (err) { if (err instanceof SlackError) { - assert.ok( - err.message.includes("Request failed with status code 400"), - ); + assert.ok(err.message.includes("An HTTP protocol error occurred")); } else { assert.fail(err); } } - assert.equal(mocks.axios.post.getCalls().length, 1); - const [url, payload, options] = mocks.axios.post.getCall(0).args; - assert.equal(url, "https://hooks.slack.com"); - assert.deepEqual(payload, { textt: "oops" }); - assert.deepEqual(options, {}); + assert.equal(mocks.webhook.incoming.getCalls().length, 1); + assert.deepEqual(mocks.webhook.incoming.getCall(0).firstArg, { + textt: "oops", + }); + assert.ok(mocks.core.setFailed.called); assert.equal(mocks.core.setOutput.getCall(0).firstArg, "ok"); assert.equal(mocks.core.setOutput.getCall(0).lastArg, false); assert.equal(mocks.core.setOutput.getCall(1).firstArg, "response"); @@ -191,8 +203,7 @@ describe("webhook", () => { mocks.core.getInput.withArgs("proxy").returns("https://example.com"); const config = new Config(mocks.core); const webhook = new Webhook(); - const request = webhook.proxies(config); - assert.strictEqual(request, undefined); + assert.strictEqual(webhook.proxies(config), undefined); }); it("sets up the proxy agent for the provided https proxy", async () => { @@ -204,12 +215,11 @@ describe("webhook", () => { mocks.core.getInput.withArgs("proxy").returns(proxy); const config = new Config(mocks.core); const webhook = new Webhook(); - const { httpsAgent, proxy: proxying } = webhook.proxies(config); + const httpsAgent = webhook.proxies(config); assert.deepEqual(httpsAgent.proxy, new URL(proxy)); - assert.notStrictEqual(proxying, false); }); - it("sets up the agent without proxy for http proxies", async () => { + it("sets up the proxy agent for the provided http proxy", async () => { const proxy = "http://example.com"; mocks.core.getInput .withArgs("webhook") @@ -218,9 +228,18 @@ describe("webhook", () => { mocks.core.getInput.withArgs("proxy").returns(proxy); const config = new Config(mocks.core); const webhook = new Webhook(); - const { httpsAgent, proxy: proxying } = webhook.proxies(config); + const httpsAgent = webhook.proxies(config); assert.deepEqual(httpsAgent.proxy, new URL(proxy)); - assert.strictEqual(proxying, false); + }); + + it("returns undefined when no proxy is set", async () => { + mocks.core.getInput + .withArgs("webhook") + .returns("https://hooks.slack.com"); + mocks.core.getInput.withArgs("webhook-type").returns("incoming-webhook"); + const config = new Config(mocks.core); + const webhook = new Webhook(); + assert.strictEqual(webhook.proxies(config), undefined); }); it("fails to configure proxies with an invalid proxied url", async () => { @@ -263,7 +282,7 @@ describe("webhook", () => { assert.ok( err.message.includes("Failed to configure the HTTPS proxy"), ); - assert.ok(err.cause.message.includes("Unsupported URL protocol")); + assert.ok(err.cause?.message?.includes("Unsupported URL protocol")); } else { assert.fail(err); } @@ -273,73 +292,63 @@ describe("webhook", () => { describe("retries", () => { it("uses a default of five retries in requests", async () => { - const webhook = new Webhook(); - const result = webhook.retries(); - assert.equal(result.retries, 5); + const result = new Webhook().retries(); + assert.equal( + result.retries, + webhook.retryPolicies.fiveRetriesInFiveMinutes.retries, + ); }); it('does not attempt retries when "0" is set', async () => { - const webhook = new Webhook(); - const result = webhook.retries("0"); + const result = new Webhook().retries("0"); assert.equal(result.retries, 0); }); it('attempts a default amount of "5" retries', async () => { - const webhook = new Webhook(); - const result = webhook.retries("5"); - assert.equal(result.retries, 5); - if (!result.retryDelay) { - assert.fail("No retry delay found!"); - } + const result = new Webhook().retries("5"); + assert.equal( + result.retries, + webhook.retryPolicies.fiveRetriesInFiveMinutes.retries, + ); assert.equal( - result.retryDelay(5, mocks.errors.axios.network_failed), - 300000, - "5th retry after 5 seconds", + result.factor, + webhook.retryPolicies.fiveRetriesInFiveMinutes.factor, ); }); it('attempts "10" retries in around "30" minutes', async () => { - const webhook = new Webhook(); - const result = webhook.retries("10"); - assert.equal(result.retries, 10); - if (!result.retryDelay) { - assert.fail("No retry delay found!"); - } - assert.ok( - result.retryDelay(10, mocks.errors.axios.network_failed) > 1800000, - "last attempt is around 30 minutes after starting", + const result = new Webhook().retries("10"); + assert.equal( + result.retries, + webhook.retryPolicies.tenRetriesInAboutThirtyMinutes.retries, ); - assert.ok( - result.retryDelay(10, mocks.errors.axios.network_failed) < 3600000, - "last attempt is no more than an hour later", + assert.equal( + result.factor, + webhook.retryPolicies.tenRetriesInAboutThirtyMinutes.factor, ); }); it('attempts a " rapid" burst of "12" retries in seconds', async () => { - const webhook = new Webhook(); - const result = webhook.retries(" rapid"); - assert.equal(result.retries, 12); - if (!result.retryDelay) { - assert.fail("No retry delay found!"); - } + const result = new Webhook().retries(" rapid"); assert.equal( - result.retryDelay(12, mocks.errors.axios.network_failed), - 12000, - "12th retry after 12 seconds", + result.retries, + webhook.retryPolicies.rapidRetryPolicy.retries, + ); + assert.equal( + result.factor, + webhook.retryPolicies.rapidRetryPolicy.factor, ); }); it('attempts a "RAPID" burst of "12" retries in seconds', async () => { - const webhook = new Webhook(); - const result = webhook.retries("RAPID"); - assert.equal(result.retries, 12); - if (!result.retryDelay) { - assert.fail("No retry delay found!"); - } + const result = new Webhook().retries("RAPID"); + assert.equal( + result.retries, + webhook.retryPolicies.rapidRetryPolicy.retries, + ); assert.equal( - result.retryDelay(12, mocks.errors.axios.network_failed), - 12000, - "12th retry after 12 seconds", + result.factor, + webhook.retryPolicies.rapidRetryPolicy.factor, ); }); });