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"; import send from "../src/send.js"; import { mocks } from "./index.spec.js"; /** * Confirm values from the action input or environment variables are gathered * or errors are thrown for invalid inputs. * * An assumption is made around these same checks and parsings being done for * each collection of configurations, but only the edge cases of checks are done * here. */ describe("config", () => { beforeEach(() => { mocks.reset(); }); describe("inputs", () => { it("valid values are collected from the action inputs", async () => { mocks.core.getInput.withArgs("api").returns("http://localhost:8080"); mocks.core.getBooleanInput.withArgs("errors").returns(true); mocks.core.getInput.withArgs("method").returns("chat.postMessage"); mocks.core.getInput.withArgs("payload").returns('"hello": "world"'); mocks.core.getInput.withArgs("proxy").returns("https://example.com"); mocks.core.getInput.withArgs("retries").returns("0"); mocks.core.getInput.withArgs("token").returns("xoxb-example"); const config = new Config(mocks.core); assert.equal(config.inputs.api, "http://localhost:8080"); assert.equal(config.inputs.errors, true); assert.equal(config.inputs.method, "chat.postMessage"); assert.equal(config.inputs.payload, '"hello": "world"'); assert.equal(config.inputs.proxy, "https://example.com"); assert.equal(config.inputs.retries, config.Retries.ZERO); assert.equal(config.inputs.token, "xoxb-example"); assert.ok(mocks.core.setSecret.withArgs("xoxb-example").called); }); it("allows token environment variables with a webhook", async () => { process.env.SLACK_TOKEN = "xoxb-example"; mocks.core.getInput.withArgs("webhook").returns("https://example.com"); mocks.core.getInput.withArgs("webhook-type").returns("incoming-webhook"); const config = new Config(mocks.core); assert.equal(config.inputs.token, "xoxb-example"); assert.equal(config.inputs.webhook, "https://example.com"); assert.equal(config.inputs.webhookType, "incoming-webhook"); assert.ok(mocks.core.setSecret.withArgs("xoxb-example").called); assert.ok(mocks.core.setSecret.withArgs("https://example.com").called); }); it("allows webhook environment variables with a token", async () => { process.env.SLACK_WEBHOOK_URL = "https://example.com"; mocks.core.getInput.withArgs("method").returns("chat.postMessage"); mocks.core.getInput.withArgs("token").returns("xoxb-example"); const config = new Config(mocks.core); assert.equal(config.inputs.method, "chat.postMessage"); assert.equal(config.inputs.token, "xoxb-example"); assert.equal(config.inputs.webhook, "https://example.com"); assert.ok(mocks.core.setSecret.withArgs("xoxb-example").called); assert.ok(mocks.core.setSecret.withArgs("https://example.com").called); }); it("errors when both the token and webhook is provided", async () => { mocks.core.getInput.withArgs("token").returns("xoxb-example"); mocks.core.getInput.withArgs("webhook").returns("https://example.com"); try { new Config(mocks.core); assert.fail("Failed to error when invalid inputs are provided"); } catch (err) { if (err instanceof SlackError) { assert.ok( err.message.includes( "Invalid input! Either the token or webhook is required - not both.", ), ); assert.ok(mocks.core.setSecret.withArgs("xoxb-example").called); assert.ok( mocks.core.setSecret.withArgs("https://example.com").called, ); } else { assert.fail(err); } } }); it("errors if the method is provided without a token", async () => { mocks.core.getInput.withArgs("method").returns("chat.postMessage"); try { new Config(mocks.core); assert.fail("Failed to error when invalid inputs are provided"); } catch (err) { if (err instanceof SlackError) { assert.ok( err.message.includes( "Missing input! A token must be provided to use the method decided.", ), ); } else { assert.fail(err); } } }); it("errors if neither the token or webhook is provided", async () => { try { new Config(mocks.core); assert.fail("Failed to error when invalid inputs are provided"); } catch (err) { if (err instanceof SlackError) { assert.ok( err.message.includes( "Missing input! Either a method or webhook is required to take action.", ), ); } else { assert.fail(err); } } }); it("errors if a webhook is provided without the type", async () => { mocks.core.getInput.withArgs("webhook").returns("https://example.com"); try { new Config(mocks.core); assert.fail("Failed to error when invalid inputs are provided"); } catch (err) { if (err instanceof SlackError) { assert.ok( err.message.includes( "Missing input! The webhook type must be 'incoming-webhook' or 'webhook-trigger'.", ), ); } else { assert.fail(err); } } }); it("errors if the webhook type does not match techniques", async () => { mocks.core.getInput.withArgs("webhook").returns("https://example.com"); mocks.core.getInput.withArgs("webhook-type").returns("post"); try { new Config(mocks.core); assert.fail("Failed to error when invalid inputs are provided"); } catch (err) { if (err instanceof SlackError) { assert.ok( err.message.includes( "Invalid input! The webhook type must be 'incoming-webhook' or 'webhook-trigger'.", ), ); } else { assert.fail(err); } } }); }); describe("instrument", () => { it("adds metadata to webapi with package name and version", async () => { const stub = sinon.stub(); const original = Object.getOwnPropertyDescriptor( webapi, "addAppMetadata", ); Object.defineProperty(webapi, "addAppMetadata", { value: stub, configurable: true, }); try { mocks.core.getInput.withArgs("method").returns("chat.postMessage"); mocks.core.getInput.withArgs("token").returns("xoxb-example"); 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(webapi, "addAppMetadata", original); } }); it("adds metadata to webhook with package name and version", async () => { 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); } }); }); describe("mask", async () => { it("treats the provided token as a secret", async () => { mocks.core.getInput.withArgs("token").returns("xoxb-example"); try { await send(mocks.core); assert.fail("Failed to error for incomplete inputs while testing"); } catch { assert.ok(mocks.core.setSecret.withArgs("xoxb-example").called); } }); it("treats the provided webhook as a secret", async () => { mocks.core.getInput.withArgs("webhook").returns("https://slack.com"); mocks.core.getInput.withArgs("webhook-type").returns("incoming-webhook"); try { await send(mocks.core); assert.fail("Failed to error for incomplete inputs while testing"); } catch { assert.ok(mocks.core.setSecret.withArgs("https://slack.com").called); } }); }); describe("validate", () => { it('allow the "retries" option with lowercased space', async () => { mocks.webhook.incoming.resolves({ text: "LGTM" }); mocks.core.getInput.withArgs("retries").returns(" rapid "); mocks.core.getInput .withArgs("webhook") .returns("https://hooks.slack.com"); mocks.core.getInput.withArgs("webhook-type").returns("incoming-webhook"); try { await send(mocks.core); } catch (err) { if (err instanceof SlackError) { assert.ok( err.message.includes( 'Invalid input! An unknown "retries" value was used: FOREVER', ), ); } else { assert.fail(err); } } }); it("errors if an invalid retries option is provided", async () => { mocks.webhook.incoming.resolves({ text: "LGTM" }); mocks.core.getInput.withArgs("retries").returns("FOREVER"); mocks.core.getInput .withArgs("webhook") .returns("https://hooks.slack.com"); mocks.core.getInput.withArgs("webhook-type").returns("incoming-webhook"); try { await send(mocks.core); } catch (err) { if (err instanceof SlackError) { assert.ok( err.message.includes( 'Invalid input! An unknown "retries" value was used: FOREVER', ), ); } else { assert.fail(err); } } }); }); });