From adf1dc21f8f74f1f81678eb25e43ef551e6f1e49 Mon Sep 17 00:00:00 2001 From: "@zimeg" Date: Tue, 29 Oct 2024 12:11:05 -0700 Subject: [PATCH] fix: set the webapi response if the request fails without exiting action --- src/client.js | 57 +++++++++++++-------- src/send.js | 4 +- test/client.spec.js | 121 +++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 153 insertions(+), 29 deletions(-) diff --git a/src/client.js b/src/client.js index b0fafe26..e5fce2bb 100644 --- a/src/client.js +++ b/src/client.js @@ -52,26 +52,43 @@ export default class Client { setName: (_name) => {}, }, }); - /** - * @type {webapi.WebAPICallResult & MessageResult} - */ - const response = await client.apiCall( - config.inputs.method, - config.content.values, - ); - config.core.setOutput("ok", response.ok); - config.core.setOutput("response", JSON.stringify(response)); - if (!response.ok) { - throw new Error(response.error); - } - if (response.channel) { - config.core.setOutput("channel_id", response.channel); - } - if (response.message?.thread_ts) { - config.core.setOutput("thread_ts", response.message.thread_ts); - } - if (response.ts) { - config.core.setOutput("ts", response.ts); + try { + /** + * @type {webapi.WebAPICallResult & MessageResult=} + */ + const response = await client.apiCall( + config.inputs.method, + config.content.values, + ); + config.core.setOutput("ok", response.ok); + config.core.setOutput("response", JSON.stringify(response)); + if (response.channel) { + config.core.setOutput("channel_id", response.channel); + } + if (response.message?.thread_ts) { + config.core.setOutput("thread_ts", response.message.thread_ts); + } + if (response.ts) { + config.core.setOutput("ts", response.ts); + } + } catch (/** @type {any} */ err) { + const slackErr = /** @type {webapi.WebAPICallError} */ (err); + config.core.setOutput("ok", false); + switch (slackErr.code) { + case webapi.ErrorCode.RequestError: + config.core.setOutput("response", JSON.stringify(slackErr.original)); + break; + case webapi.ErrorCode.HTTPError: + config.core.setOutput("response", JSON.stringify(slackErr)); + break; + case webapi.ErrorCode.PlatformError: + config.core.setOutput("response", JSON.stringify(slackErr.data)); + break; + case webapi.ErrorCode.RateLimitedError: + config.core.setOutput("response", JSON.stringify(slackErr)); + break; + } + throw new Error(err); } } diff --git a/src/send.js b/src/send.js index 456899dd..7b85ed0a 100644 --- a/src/send.js +++ b/src/send.js @@ -16,7 +16,9 @@ export default async function send(core) { config.core.setOutput("time", Math.floor(new Date().valueOf() / 1000)); } catch (error) { config.core.setOutput("time", Math.floor(new Date().valueOf() / 1000)); - throw new SlackError(core, error, config.inputs.errors); + if (config.inputs.errors) { + throw new SlackError(core, error); + } } } diff --git a/test/client.spec.js b/test/client.spec.js index ec802724..bbddd796 100644 --- a/test/client.spec.js +++ b/test/client.spec.js @@ -1,5 +1,6 @@ import core from "@actions/core"; import webapi from "@slack/web-api"; +import errors from "@slack/web-api/dist/errors.js"; import { assert } from "chai"; import Client from "../src/client.js"; import Config from "../src/config.js"; @@ -141,10 +142,87 @@ describe("client", () => { }); describe("failure", () => { + it("errors when the request to the api cannot be sent correct", async () => { + /** + * @type {webapi.WebAPICallError} + */ + const response = { + code: "slack_webapi_request_error", + data: { + error: "unexpected_request_failure", + message: "Something bad happened!", + }, + }; + try { + mocks.core.getInput.reset(); + mocks.core.getBooleanInput.withArgs("errors").returns(true); + mocks.core.getInput.withArgs("method").returns("chat.postMessage"); + mocks.core.getInput.withArgs("token").returns("xoxb-example"); + mocks.core.getInput.withArgs("payload").returns(`"text": "hello"`); + mocks.api.rejects(errors.requestErrorWithOriginal(response, true)); + await send(mocks.core); + assert.fail("Expected an error but none was found"); + } catch (error) { + assert.isTrue(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"); + assert.deepEqual( + mocks.core.setOutput.getCall(1).lastArg, + JSON.stringify(response), + ); + assert.equal(mocks.core.setOutput.getCall(2).firstArg, "time"); + assert.equal(mocks.core.setOutput.getCalls().length, 3); + } + }); + + it("errors when the http portion of the request fails to send", async () => { + /** + * @type {import("axios").AxiosResponse} + */ + const response = { + code: "slack_webapi_http_error", + headers: { + authorization: "none", + }, + data: { + ok: false, + error: "unknown_http_method", + }, + }; + try { + mocks.core.getInput.withArgs("method").returns("chat.postMessage"); + mocks.core.getInput.withArgs("token").returns("xoxb-example"); + mocks.core.getInput.withArgs("payload").returns(`"text": "hello"`); + mocks.api.rejects(errors.httpErrorFromResponse(response)); + await send(mocks.core); + assert.fail("Expected an error but none was found"); + } catch (error) { + assert.isFalse(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"); + response.body = response.data; + response.data = undefined; + assert.deepEqual( + mocks.core.setOutput.getCall(1).lastArg, + JSON.stringify(response), + ); + assert.equal(mocks.core.setOutput.getCall(2).firstArg, "time"); + assert.equal(mocks.core.setOutput.getCalls().length, 3); + } + }); + it("errors when the payload arguments are invalid for the api", async () => { + /** + * @type {webapi.WebAPICallError} + */ const response = { - ok: false, - error: "missing_channel", + code: "slack_webapi_platform_error", + data: { + ok: false, + error: "missing_channel", + }, }; try { mocks.core.getInput.reset(); @@ -152,7 +230,7 @@ describe("client", () => { mocks.core.getInput.withArgs("method").returns("chat.postMessage"); mocks.core.getInput.withArgs("token").returns("xoxb-example"); mocks.core.getInput.withArgs("payload").returns(`"text": "hello"`); - mocks.api.resolves(response); + mocks.api.rejects(errors.platformErrorFromResult(response)); await send(mocks.core); assert.fail("Expected an error but none was found"); } catch (error) { @@ -171,16 +249,43 @@ describe("client", () => { it("returns the api error and details without a exit failing", async () => { const response = { - ok: false, - error: "missing_channel", + code: "slack_webapi_platform_error", + data: { + ok: false, + error: "missing_channel", + }, }; try { - mocks.core.getInput.reset(); - mocks.core.getBooleanInput.withArgs("errors").returns(false); mocks.core.getInput.withArgs("method").returns("chat.postMessage"); mocks.core.getInput.withArgs("token").returns("xoxb-example"); mocks.core.getInput.withArgs("payload").returns(`"text": "hello"`); - mocks.api.resolves(response); + mocks.api.rejects(errors.platformErrorFromResult(response)); + await send(mocks.core); + assert.fail("Expected an error but none was found"); + } catch (error) { + assert.isFalse(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"); + assert.deepEqual( + mocks.core.setOutput.getCall(1).lastArg, + JSON.stringify(response), + ); + assert.equal(mocks.core.setOutput.getCall(2).firstArg, "time"); + assert.equal(mocks.core.setOutput.getCalls().length, 3); + } + }); + + it("errors if rate limit responses are returned after retries", async () => { + const response = { + code: "slack_webapi_rate_limited_error", + retryAfter: 12, + }; + try { + mocks.core.getInput.withArgs("method").returns("chat.postMessage"); + mocks.core.getInput.withArgs("token").returns("xoxb-example"); + mocks.core.getInput.withArgs("payload").returns(`"text": "hello"`); + mocks.api.rejects(errors.rateLimitedErrorWithDelay(12)); await send(mocks.core); assert.fail("Expected an error but none was found"); } catch (error) {