import assert from "node:assert"; import { beforeEach, describe, it } from "node:test"; import webapi from "@slack/web-api"; import sinon from "sinon"; 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.core.getInput.withArgs("token").returns("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 = sinon.stub().resolves({ ok: true }); const constructors = sinon .stub(mocks.webapi, "WebClient") .returns({ 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.ok(constructors.calledWithNew()); assert.ok( constructors.calledWith("xoxb-example-002", { fetch: undefined, allowAbsoluteUrls: false, logger: config.logger, retryConfig: webapi.retryPolicies.fiveRetriesInFiveMinutes, slackApiUrl: undefined, }), ); assert.ok(apis.calledOnce); assert.ok( apis.calledWith("pins.add", { channel: "CHANNELHERE", timestamp: "1234567890.000000", }), ); assert.ok(config.core.setOutput.calledWith("ok", true)); }); it("uses arguments to send to a custom api method", async () => { const apis = sinon.stub().resolves({ done: true, response: "Infinite" }); const constructors = sinon .stub(mocks.webapi, "WebClient") .returns({ 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.ok(constructors.calledWithNew()); assert.ok( constructors.calledWith("ollamapassword", { fetch: undefined, allowAbsoluteUrls: false, logger: config.logger, retryConfig: webapi.retryPolicies.tenRetriesInAboutThirtyMinutes, slackApiUrl: "http://localhost:11434/api/", }), ); assert.ok(apis.calledOnce); assert.ok( apis.calledWith("generate", { model: "llama3.2", prompt: "How many sides does a circle have?", stream: false, }), ); assert.ok(config.core.setOutput.calledWith("ok", undefined)); assert.ok( config.core.setOutput.calledWith( "response", JSON.stringify({ done: true, response: "Infinite" }), ), ); }); }); describe("success", () => { it("calls 'chat.postMessage' with the given token and content", async () => { try { const args = { channel: "C0123456789", text: "hello", thread_ts: "1234567890.000001", }; const response = { ok: true, channel: "C0123456789", ts: "1234567890.000002", message: { thread_ts: "1234567890.000001", }, }; mocks.core.getInput.withArgs("method").returns("chat.postMessage"); mocks.core.getInput.withArgs("token").returns("xoxb-example"); mocks.core.getInput.withArgs("payload").returns(JSON.stringify(args)); mocks.calls.resolves(response); await send(mocks.core); assert.deepEqual(mocks.calls.getCall(0).firstArg, "chat.postMessage"); assert.deepEqual(mocks.calls.getCall(0).lastArg, args); 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"); assert.equal( mocks.core.setOutput.getCall(1).lastArg, JSON.stringify(response), ); assert.equal(mocks.core.setOutput.getCall(2).firstArg, "channel_id"); assert.equal(mocks.core.setOutput.getCall(2).lastArg, "C0123456789"); assert.equal(mocks.core.setOutput.getCall(3).firstArg, "thread_ts"); assert.equal( mocks.core.setOutput.getCall(3).lastArg, "1234567890.000001", ); assert.equal(mocks.core.setOutput.getCall(4).firstArg, "ts"); assert.equal( mocks.core.setOutput.getCall(4).lastArg, "1234567890.000002", ); assert.equal(mocks.core.setOutput.getCall(5).firstArg, "time"); assert.equal(mocks.core.setOutput.getCalls().length, 6); } catch (err) { console.error(err); assert.fail("Unexpected error when calling the method"); } }); it("calls 'conversations.create' with the given token and content", async () => { try { const args = { name: "pull-request-review-010101", }; const response = { ok: true, channel: { id: "C0101010101", name: "pull-request-review-010101", is_channel: true, created: 1730425428, }, }; mocks.core.getInput.withArgs("method").returns("chat.postMessage"); mocks.core.getInput.withArgs("token").returns("xoxb-example"); mocks.core.getInput.withArgs("payload").returns(JSON.stringify(args)); mocks.calls.resolves(response); await send(mocks.core); assert.deepEqual(mocks.calls.getCall(0).firstArg, "chat.postMessage"); assert.deepEqual(mocks.calls.getCall(0).lastArg, args); 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"); assert.equal( mocks.core.setOutput.getCall(1).lastArg, JSON.stringify(response), ); assert.equal(mocks.core.setOutput.getCall(2).firstArg, "channel_id"); assert.equal(mocks.core.setOutput.getCall(2).lastArg, "C0101010101"); assert.equal(mocks.core.setOutput.getCall(3).firstArg, "time"); assert.equal(mocks.core.setOutput.getCalls().length, 4); } catch (err) { console.error(err); assert.fail("Unexpected error when calling the method"); } }); it("calls 'files.uploadV2' with the provided token and content", async () => { try { 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 }], }; mocks.core.getInput.withArgs("method").returns("files.uploadV2"); mocks.core.getInput.withArgs("token").returns("xoxp-example"); mocks.core.getInput.withArgs("payload").returns(JSON.stringify(args)); mocks.calls.resolves(response); await send(mocks.core); assert.deepEqual(mocks.calls.getCall(0).lastArg, args); 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"); assert.equal( 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); } catch (err) { console.error(err); assert.fail("Unexpected error when calling the method"); } }); }); describe("failure", () => { it("errors when the request to the api cannot be sent correct", async () => { const response = new Error("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.calls.rejects(new webapi.WebAPIRequestError(response)); await send(mocks.core); assert.fail("Expected an error but none was found"); } catch (_err) { 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"); 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 () => { const response = new webapi.WebAPIHTTPError( 500, "Internal Server Error", { authorization: "none", }, { 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.calls.rejects(response); await send(mocks.core); assert.fail("Expected an error but none was found"); } catch (_err) { assert.strictEqual(mocks.core.setFailed.called, false); 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.equal( JSON.parse(mocks.core.setOutput.getCall(1).lastArg).statusCode, 500, ); 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 () => { const response = { ok: false, error: "missing_channel" }; 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.calls.rejects(new webapi.WebAPIPlatformError(response)); await send(mocks.core); assert.fail("Expected an error but none was found"); } catch (_err) { 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"); 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("returns the api error and details without a exit failing", async () => { const response = { ok: false, error: "missing_channel" }; 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.calls.rejects(new webapi.WebAPIPlatformError(response)); await send(mocks.core); assert.fail("Expected an error but none was found"); } catch (_err) { assert.strictEqual(mocks.core.setFailed.called, false); 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 () => { 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.calls.rejects(new webapi.WebAPIRateLimitedError(12)); await send(mocks.core); assert.fail("Expected an error but none was found"); } catch (_err) { assert.strictEqual(mocks.core.setFailed.called, false); 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.equal( JSON.parse(mocks.core.setOutput.getCall(1).lastArg).retryAfter, 12, ); assert.equal(mocks.core.setOutput.getCall(2).firstArg, "time"); assert.equal(mocks.core.setOutput.getCalls().length, 3); } }); }); 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 client = new Client(); const result = client.retries("0"); assert.equal(result.retries, 0); }); it('attempts a default amount of "5" retries', async () => { const client = new Client(); const result = client.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 client = new Client(); const result = client.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 client = new Client(); const result = client.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 client = new Client(); const result = client.retries("RAPID"); assert.equal( result.retries, webapi.retryPolicies.rapidRetryPolicy.retries, ); assert.equal(result.factor, webapi.retryPolicies.rapidRetryPolicy.factor); }); }); });