Files
slack-github-action/test/client.spec.js
2026-02-04 00:18:35 -08:00

628 lines
20 KiB
JavaScript

import assert from "node:assert";
import { beforeEach, describe, it, mock } from "node:test";
import webapi from "@slack/web-api";
import errors from "@slack/web-api/dist/errors.js";
import Client from "../src/client.js";
import Config from "../src/config.js";
import SlackError from "../src/errors.js";
import Logger from "../src/logger.js";
import send from "../src/send.js";
import { mocks } from "./index.spec.js";
describe("client", () => {
beforeEach(() => {
mocks.reset();
});
describe("inputs", () => {
it("requires a method is provided in inputs", async () => {
/**
* @type {Config}
*/
const config = {
core: mocks.core,
inputs: {
token: "xoxb-example",
},
};
try {
await new Client().post(config);
assert.fail("Failed to throw for missing input");
} catch (err) {
if (err instanceof SlackError) {
assert.ok(err.message.includes("No API method was provided for use"));
} else {
assert.fail(err);
}
}
});
it("requires a token is provided in inputs", async () => {
/**
* @type {Config}
*/
const config = {
core: mocks.core,
inputs: {
method: "chat.postMessage",
},
};
mocks.inputs = {
...mocks.inputs,
token: "xoxb-example-001",
};
try {
await new Client().post(config);
assert.fail("Failed to throw for missing input");
} catch (err) {
if (err instanceof SlackError) {
assert.ok(err.message.includes("No token was provided to post with"));
} else {
assert.fail(err);
}
}
});
});
describe("api", async () => {
it("uses arguments to send to a slack api method", async () => {
const apis = mock.fn(() => Promise.resolve({ ok: true }));
const constructorCalls = [];
// Use a regular function as constructor since mock.fn() doesn't work with `new`
mocks.webapi.WebClient = function (...args) {
constructorCalls.push(args);
this.apiCall = apis;
};
/**
* @type {Config}
*/
const config = {
content: {
values: {
channel: "CHANNELHERE",
timestamp: "1234567890.000000",
},
},
core: mocks.core,
logger: new Logger(mocks.core).logger,
inputs: {
method: "pins.add",
token: "xoxb-example-002",
},
webapi: mocks.webapi,
};
await new Client().post(config);
assert.equal(constructorCalls.length, 1);
const [token, options] = constructorCalls[0];
assert.equal(token, "xoxb-example-002");
assert.deepEqual(options, {
agent: undefined,
allowAbsoluteUrls: false,
logger: config.logger,
retryConfig: webapi.retryPolicies.fiveRetriesInFiveMinutes,
slackApiUrl: undefined,
});
assert.equal(apis.mock.callCount(), 1);
const [method, args] = apis.mock.calls[0].arguments;
assert.equal(method, "pins.add");
assert.deepEqual(args, {
channel: "CHANNELHERE",
timestamp: "1234567890.000000",
});
assert.ok(
mocks.core.setOutput.mock.calls.some(
(c) => c.arguments[0] === "ok" && c.arguments[1] === true,
),
);
});
it("uses arguments to send to a custom api method", async () => {
const apis = mock.fn(() =>
Promise.resolve({ done: true, response: "Infinite" }),
);
const constructorCalls = [];
// Use a regular function as constructor since mock.fn() doesn't work with `new`
mocks.webapi.WebClient = function (...args) {
constructorCalls.push(args);
this.apiCall = apis;
};
/**
* @type {Config}
*/
const config = {
content: {
values: {
model: "llama3.2",
prompt: "How many sides does a circle have?",
stream: false,
},
},
core: mocks.core,
logger: new Logger(mocks.core).logger,
inputs: {
api: "http://localhost:11434/api/",
method: "generate",
retries: "10",
token: "ollamapassword",
},
webapi: mocks.webapi,
};
await new Client().post(config);
assert.equal(constructorCalls.length, 1);
const [token, options] = constructorCalls[0];
assert.equal(token, "ollamapassword");
assert.deepEqual(options, {
agent: undefined,
allowAbsoluteUrls: false,
logger: config.logger,
retryConfig: webapi.retryPolicies.tenRetriesInAboutThirtyMinutes,
slackApiUrl: "http://localhost:11434/api/",
});
assert.equal(apis.mock.callCount(), 1);
const [method, args] = apis.mock.calls[0].arguments;
assert.equal(method, "generate");
assert.deepEqual(args, {
model: "llama3.2",
prompt: "How many sides does a circle have?",
stream: false,
});
assert.ok(
mocks.core.setOutput.mock.calls.some(
(c) => c.arguments[0] === "ok" && c.arguments[1] === undefined,
),
);
assert.ok(
mocks.core.setOutput.mock.calls.some(
(c) =>
c.arguments[0] === "response" &&
c.arguments[1] ===
JSON.stringify({ done: true, response: "Infinite" }),
),
);
});
});
describe("success", () => {
it("calls 'chat.postMessage' with the given token and content", async () => {
const args = {
channel: "C0123456789",
text: "hello",
thread_ts: "1234567890.000001",
};
const response = {
ok: true,
channel: "C0123456789",
ts: "1234567890.000002",
message: {
thread_ts: "1234567890.000001",
},
};
const payload = JSON.stringify(args);
mocks.inputs = {
...mocks.inputs,
method: "chat.postMessage",
token: "xoxb-example",
payload,
};
mocks.calls._resolvesWith = response;
await send(mocks.core);
assert.deepEqual(
mocks.calls.mock.calls[0].arguments[0],
"chat.postMessage",
);
assert.deepEqual(mocks.calls.mock.calls[0].arguments[1], args);
assert.equal(mocks.core.setOutput.mock.calls[0].arguments[0], "ok");
assert.equal(mocks.core.setOutput.mock.calls[0].arguments[1], true);
assert.equal(mocks.core.setOutput.mock.calls[1].arguments[0], "response");
assert.equal(
mocks.core.setOutput.mock.calls[1].arguments[1],
JSON.stringify(response),
);
assert.equal(
mocks.core.setOutput.mock.calls[2].arguments[0],
"channel_id",
);
assert.equal(
mocks.core.setOutput.mock.calls[2].arguments[1],
"C0123456789",
);
assert.equal(
mocks.core.setOutput.mock.calls[3].arguments[0],
"thread_ts",
);
assert.equal(
mocks.core.setOutput.mock.calls[3].arguments[1],
"1234567890.000001",
);
assert.equal(mocks.core.setOutput.mock.calls[4].arguments[0], "ts");
assert.equal(
mocks.core.setOutput.mock.calls[4].arguments[1],
"1234567890.000002",
);
assert.equal(mocks.core.setOutput.mock.calls[5].arguments[0], "time");
assert.equal(mocks.core.setOutput.mock.calls.length, 6);
});
it("calls 'conversations.create' with the given token and content", async () => {
const args = {
name: "pull-request-review-010101",
};
const response = {
ok: true,
channel: {
id: "C0101010101",
name: "pull-request-review-010101",
is_channel: true,
created: 1730425428,
},
};
const payload = JSON.stringify(args);
mocks.inputs = {
...mocks.inputs,
method: "chat.postMessage",
token: "xoxb-example",
payload,
};
mocks.calls._resolvesWith = response;
await send(mocks.core);
assert.deepEqual(
mocks.calls.mock.calls[0].arguments[0],
"chat.postMessage",
);
assert.deepEqual(mocks.calls.mock.calls[0].arguments[1], args);
assert.equal(mocks.core.setOutput.mock.calls[0].arguments[0], "ok");
assert.equal(mocks.core.setOutput.mock.calls[0].arguments[1], true);
assert.equal(mocks.core.setOutput.mock.calls[1].arguments[0], "response");
assert.equal(
mocks.core.setOutput.mock.calls[1].arguments[1],
JSON.stringify(response),
);
assert.equal(
mocks.core.setOutput.mock.calls[2].arguments[0],
"channel_id",
);
assert.equal(
mocks.core.setOutput.mock.calls[2].arguments[1],
"C0101010101",
);
assert.equal(mocks.core.setOutput.mock.calls[3].arguments[0], "time");
assert.equal(mocks.core.setOutput.mock.calls.length, 4);
});
it("calls 'files.uploadV2' with the provided token and content", async () => {
const args = {
channel: "C0000000001",
initial_comment: "the results are in!",
file: "results.out",
filename: "results-888888.out",
};
const response = {
ok: true,
files: [{ id: "F0000000001", created: 1234567890 }],
};
const payload = JSON.stringify(args);
mocks.inputs = {
...mocks.inputs,
method: "files.uploadV2",
token: "xoxp-example",
payload,
};
mocks.calls._resolvesWith = response;
await send(mocks.core);
assert.deepEqual(mocks.calls.mock.calls[0].arguments[1], args);
assert.equal(mocks.core.setOutput.mock.calls[0].arguments[0], "ok");
assert.equal(mocks.core.setOutput.mock.calls[0].arguments[1], true);
assert.equal(mocks.core.setOutput.mock.calls[1].arguments[0], "response");
assert.equal(
mocks.core.setOutput.mock.calls[1].arguments[1],
JSON.stringify(response),
);
assert.equal(mocks.core.setOutput.mock.calls[2].arguments[0], "time");
assert.equal(mocks.core.setOutput.mock.calls.length, 3);
});
});
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!",
},
};
mocks.booleanInputs = {
...mocks.booleanInputs,
errors: true,
};
mocks.inputs = {
...mocks.inputs,
method: "chat.postMessage",
token: "xoxb-example",
payload: `"text": "hello"`,
};
mocks.calls._rejectsWith = errors.requestErrorWithOriginal(
response,
true,
);
try {
await send(mocks.core);
assert.fail("Expected an error but none was found");
} catch (_err) {
assert.ok(mocks.core.setFailed.mock.callCount() > 0);
assert.equal(mocks.core.setOutput.mock.calls[0].arguments[0], "ok");
assert.equal(mocks.core.setOutput.mock.calls[0].arguments[1], false);
assert.equal(
mocks.core.setOutput.mock.calls[1].arguments[0],
"response",
);
assert.deepEqual(
mocks.core.setOutput.mock.calls[1].arguments[1],
JSON.stringify(response),
);
assert.equal(mocks.core.setOutput.mock.calls[2].arguments[0], "time");
assert.equal(mocks.core.setOutput.mock.calls.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",
},
};
mocks.inputs = {
...mocks.inputs,
method: "chat.postMessage",
token: "xoxb-example",
payload: `"text": "hello"`,
};
mocks.calls._rejectsWith = errors.httpErrorFromResponse(response);
try {
await send(mocks.core);
assert.fail("Expected an error but none was found");
} catch (_err) {
assert.strictEqual(mocks.core.setFailed.mock.callCount(), 0);
assert.equal(mocks.core.setOutput.mock.calls[0].arguments[0], "ok");
assert.equal(mocks.core.setOutput.mock.calls[0].arguments[1], false);
assert.equal(
mocks.core.setOutput.mock.calls[1].arguments[0],
"response",
);
response.body = response.data;
response.data = undefined;
assert.deepEqual(
mocks.core.setOutput.mock.calls[1].arguments[1],
JSON.stringify(response),
);
assert.equal(mocks.core.setOutput.mock.calls[2].arguments[0], "time");
assert.equal(mocks.core.setOutput.mock.calls.length, 3);
}
});
it("errors when the payload arguments are invalid for the api", async () => {
/**
* @type {webapi.WebAPICallError}
*/
const response = {
code: "slack_webapi_platform_error",
data: {
ok: false,
error: "missing_channel",
},
};
mocks.booleanInputs = {
...mocks.booleanInputs,
errors: true,
};
mocks.inputs = {
...mocks.inputs,
method: "chat.postMessage",
token: "xoxb-example",
payload: `"text": "hello"`,
};
mocks.calls._rejectsWith = errors.platformErrorFromResult(response);
try {
await send(mocks.core);
assert.fail("Expected an error but none was found");
} catch (_err) {
assert.ok(mocks.core.setFailed.mock.callCount() > 0);
assert.equal(mocks.core.setOutput.mock.calls[0].arguments[0], "ok");
assert.equal(mocks.core.setOutput.mock.calls[0].arguments[1], false);
assert.equal(
mocks.core.setOutput.mock.calls[1].arguments[0],
"response",
);
assert.deepEqual(
mocks.core.setOutput.mock.calls[1].arguments[1],
JSON.stringify(response),
);
assert.equal(mocks.core.setOutput.mock.calls[2].arguments[0], "time");
assert.equal(mocks.core.setOutput.mock.calls.length, 3);
}
});
it("returns the api error and details without a exit failing", async () => {
const response = {
code: "slack_webapi_platform_error",
data: {
ok: false,
error: "missing_channel",
},
};
mocks.inputs = {
...mocks.inputs,
method: "chat.postMessage",
token: "xoxb-example",
payload: `"text": "hello"`,
};
mocks.calls._rejectsWith = errors.platformErrorFromResult(response);
try {
await send(mocks.core);
assert.fail("Expected an error but none was found");
} catch (_err) {
assert.strictEqual(mocks.core.setFailed.mock.callCount(), 0);
assert.equal(mocks.core.setOutput.mock.calls[0].arguments[0], "ok");
assert.equal(mocks.core.setOutput.mock.calls[0].arguments[1], false);
assert.equal(
mocks.core.setOutput.mock.calls[1].arguments[0],
"response",
);
assert.deepEqual(
mocks.core.setOutput.mock.calls[1].arguments[1],
JSON.stringify(response),
);
assert.equal(mocks.core.setOutput.mock.calls[2].arguments[0], "time");
assert.equal(mocks.core.setOutput.mock.calls.length, 3);
}
});
it("errors if rate limit responses are returned after retries", async () => {
const response = {
code: "slack_webapi_rate_limited_error",
retryAfter: 12,
};
mocks.inputs = {
...mocks.inputs,
method: "chat.postMessage",
token: "xoxb-example",
payload: `"text": "hello"`,
};
mocks.calls._rejectsWith = errors.rateLimitedErrorWithDelay(12);
try {
await send(mocks.core);
assert.fail("Expected an error but none was found");
} catch (_err) {
assert.strictEqual(mocks.core.setFailed.mock.callCount(), 0);
assert.equal(mocks.core.setOutput.mock.calls[0].arguments[0], "ok");
assert.equal(mocks.core.setOutput.mock.calls[0].arguments[1], false);
assert.equal(
mocks.core.setOutput.mock.calls[1].arguments[0],
"response",
);
assert.deepEqual(
mocks.core.setOutput.mock.calls[1].arguments[1],
JSON.stringify(response),
);
assert.equal(mocks.core.setOutput.mock.calls[2].arguments[0], "time");
assert.equal(mocks.core.setOutput.mock.calls.length, 3);
}
});
});
describe("proxies", () => {
it("sets up the proxy agent for the provided https proxy", async () => {
const proxy = "https://example.com";
mocks.inputs = {
...mocks.inputs,
method: "chat.postMessage",
proxy,
token: "xoxb-example",
};
const config = new Config(mocks.core);
const client = new Client();
const { httpsAgent, proxy: proxying } = client.proxies(config);
assert.deepEqual(httpsAgent.proxy, new URL(proxy));
assert.notStrictEqual(proxying, false);
});
it("fails to configure proxies with an invalid proxied url", async () => {
const proxy = "https://";
mocks.inputs = {
...mocks.inputs,
method: "chat.postMessage",
proxy,
token: "xoxb-example",
};
try {
const config = new Config(mocks.core);
const client = new Client();
client.proxies(config);
assert.fail("An invalid proxy URL was not thrown as error!");
} catch (err) {
if (err instanceof SlackError) {
assert.ok(
err.message.includes("Failed to configure the HTTPS proxy"),
);
} else {
assert.fail(err);
}
}
});
});
describe("retries", () => {
it("uses a default of five retries in requests", async () => {
const client = new Client();
const result = client.retries();
assert.equal(
result.retries,
webapi.retryPolicies.fiveRetriesInFiveMinutes.retries,
);
});
it('does not attempt retries when "0" is set', async () => {
const webhook = new Client();
const result = webhook.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");
assert.equal(
result.retries,
webapi.retryPolicies.fiveRetriesInFiveMinutes.retries,
);
assert.equal(
result.factor,
webapi.retryPolicies.fiveRetriesInFiveMinutes.factor,
);
});
it('attempts "10" retries in around "30" minutes', async () => {
const webhook = new Client();
const result = webhook.retries("10");
assert.equal(
result.retries,
webapi.retryPolicies.tenRetriesInAboutThirtyMinutes.retries,
);
assert.equal(
result.factor,
webapi.retryPolicies.tenRetriesInAboutThirtyMinutes.factor,
);
});
it('attempts a "rapid " burst of "12" retries in seconds', async () => {
const webhook = new Client();
const result = webhook.retries("rapid ");
assert.equal(
result.retries,
webapi.retryPolicies.rapidRetryPolicy.retries,
);
assert.equal(result.factor, webapi.retryPolicies.rapidRetryPolicy.factor);
});
it('attempts a "RAPID" burst of "12" retries in seconds', async () => {
const webhook = new Client();
const result = webhook.retries("RAPID");
assert.equal(
result.retries,
webapi.retryPolicies.rapidRetryPolicy.retries,
);
assert.equal(result.factor, webapi.retryPolicies.rapidRetryPolicy.factor);
});
});
});