Files
slack-github-action/test/content.spec.js

632 lines
18 KiB
JavaScript

import assert from "node:assert";
import { dirname, join } from "node:path";
import { beforeEach, describe, it } from "node:test";
import { fileURLToPath } from "node:url";
import { YAMLException } from "js-yaml";
import Config from "../src/config.js";
import Content from "../src/content.js";
import SlackError from "../src/errors.js";
import send from "../src/send.js";
import { mocks } from "./index.spec.js";
const __dirname = dirname(fileURLToPath(import.meta.url));
const FIXTURES = join(__dirname, "fixtures");
/**
* Confirm values from the action input or environment variables are gathered
*/
describe("content", () => {
beforeEach(() => {
mocks.reset();
mocks.inputs = {
...mocks.inputs,
method: "chat.postMessage",
token: "xoxb-example",
};
// Set up default apiCall mock for tests that use send()
mocks.calls._resolvesWith = { ok: true };
});
describe("flatten", () => {
it("flattens nested payloads provided with delimiter", async () => {
mocks.inputs = {
...mocks.inputs,
payload: `
"apples": "tree",
"bananas": {
"truthiness": true
}
`,
"payload-delimiter": "_",
};
const config = new Config(mocks.core);
const expected = {
apples: "tree",
bananas_truthiness: "true",
};
assert.deepEqual(config.content.values, expected);
});
});
describe("get", () => {
it("errors if both a payload and file path are provided", async () => {
mocks.inputs = {
...mocks.inputs,
payload: `"message"="hello"`,
"payload-file-path": "example.json",
};
try {
await send(mocks.core);
assert.fail("Failed to throw for invalid input");
} catch (err) {
if (err instanceof SlackError) {
assert.ok(
err.message.includes(
"Invalid input! Just the payload or payload file path is required.",
),
);
} else {
assert.fail(err);
}
}
});
});
describe("payload", async () => {
it("parses complete YAML from the input payload", async () => {
mocks.inputs = {
...mocks.inputs,
payload: `
message: "this is wrapped"
channel: "C0123456789"
`,
};
const config = new Config(mocks.core);
const expected = {
message: "this is wrapped",
channel: "C0123456789",
};
assert.deepEqual(config.content.values, expected);
});
it("parses complete JSON from the input payload", async () => {
mocks.inputs = {
...mocks.inputs,
payload: `{
"message": "this is wrapped",
"channel": "C0123456789"
}
`,
};
const config = new Config(mocks.core);
const expected = {
message: "this is wrapped",
channel: "C0123456789",
};
assert.deepEqual(config.content.values, expected);
});
it("templatizes variables requires configuration", async () => {
mocks.inputs = {
...mocks.inputs,
payload: `{
"message": "this matches an existing variable: \${{ github.apiUrl }}",
"channel": "C0123456789"
}
`,
};
const config = new Config(mocks.core);
// biome-ignore-start lint/suspicious/noTemplateCurlyInString: GitHub Action YAML variable syntax
const expected = {
message: "this matches an existing variable: ${{ github.apiUrl }}",
channel: "C0123456789",
};
// biome-ignore-end lint/suspicious/noTemplateCurlyInString: https://docs.github.com/en/actions/how-tos/writing-workflows/choosing-what-your-workflow-does/store-information-in-variables#using-contexts-to-access-variable-values
assert.deepEqual(config.content.values, expected);
});
it("templatizes variables with matching variables", async () => {
mocks.inputs = {
...mocks.inputs,
payload: `
channel: C0123456789
reply_broadcast: false
message: Served \${{ env.NUMBER }} items
blocks:
- type: section
text:
type: mrkdwn
text: "Served \${{ env.NUMBER }} items on: \${{ env.DETAILS }}"
- type: divider
- type: section
block_id: selector
text:
type: mrkdwn
text: Send feedback
accessory:
action_id: response
type: multi_static_select
placeholder:
type: plain_text
text: Select URL
options:
- text:
type: plain_text
text: "\${{ github.apiUrl }}"
value: api
- text:
type: plain_text
text: "\${{ github.serverUrl }}"
value: server
- text:
type: plain_text
text: "\${{ github.graphqlUrl }}"
value: graphql
`,
};
mocks.booleanInputs = {
...mocks.booleanInputs,
"payload-templated": true,
};
process.env.DETAILS = `
-fri
-sat
-sun`;
process.env.NUMBER = 12;
const config = new Config(mocks.core);
process.env.DETAILS = undefined;
process.env.NUMBER = undefined;
const expected = {
channel: "C0123456789",
reply_broadcast: false,
message: "Served 12 items",
blocks: [
{
type: "section",
text: {
type: "mrkdwn",
text: "Served 12 items on: \n-fri\n-sat\n-sun",
},
},
{
type: "divider",
},
{
type: "section",
block_id: "selector",
text: {
type: "mrkdwn",
text: "Send feedback",
},
accessory: {
action_id: "response",
type: "multi_static_select",
placeholder: {
type: "plain_text",
text: "Select URL",
},
options: [
{
text: {
type: "plain_text",
text: "https://api.github.com",
},
value: "api",
},
{
text: {
type: "plain_text",
text: "https://github.com",
},
value: "server",
},
{
text: {
type: "plain_text",
text: "https://api.github.com/graphql",
},
value: "graphql",
},
],
},
},
],
};
assert.deepEqual(config.content.values, expected);
});
/**
* @see {@link https://github.com/slackapi/slack-github-action/issues/203}
*/
it("templatizes variables with missing variables", async () => {
// biome-ignore-start lint/suspicious/noTemplateCurlyInString: GitHub Action YAML variable syntax
mocks.inputs = {
...mocks.inputs,
payload: "message: What makes ${{ env.TREASURE }} a secret",
};
// biome-ignore-end lint/suspicious/noTemplateCurlyInString: https://docs.github.com/en/actions/how-tos/writing-workflows/choosing-what-your-workflow-does/store-information-in-variables#using-contexts-to-access-variable-values
mocks.booleanInputs = {
...mocks.booleanInputs,
"payload-templated": true,
};
const config = new Config(mocks.core);
const expected = {
message: "What makes ??? a secret",
};
assert.deepEqual(config.content.values, expected);
});
it("trims last comma JSON with the input payload", async () => {
mocks.inputs = {
...mocks.inputs,
payload: `
"message": "LGTM!",
"channel": "C0123456789",
`,
};
const config = new Config(mocks.core);
const expected = {
message: "LGTM!",
channel: "C0123456789",
};
assert.deepEqual(config.content.values, expected);
});
it("wraps incomplete JSON from the input payload", async () => {
mocks.inputs = {
...mocks.inputs,
payload: `
"message": "LGTM!",
"channel": "C0123456789",
"blocks": [
{
"type": "section",
"text": {
"text": "LGTM! :+1:"
}
}
]
`,
};
const config = new Config(mocks.core);
const expected = {
message: "LGTM!",
channel: "C0123456789",
blocks: [
{
type: "section",
text: {
text: "LGTM! :+1:",
},
},
],
};
assert.deepEqual(config.content.values, expected);
});
it("fails if no payload content is provided in input", async () => {
/**
* @type {Config}
*/
const config = {
core: mocks.core,
inputs: {
payloadFilePath: "unknown.json",
},
};
try {
new Content().getContentPayload(config);
assert.fail("Failed to throw for missing payload content");
} catch (err) {
if (err instanceof SlackError) {
assert.ok(
err.message.includes(
"Invalid input! No payload content was provided",
),
);
} else {
assert.fail(err);
}
}
});
it("fails if invalid JSON exists in the input payload", async () => {
mocks.inputs = {
...mocks.inputs,
payload: "{",
};
try {
await send(mocks.core);
assert.fail("Failed to throw for invalid JSON");
} catch (err) {
if (err instanceof SlackError) {
assert.ok(
err.message.includes(
"Invalid input! Failed to parse contents of the provided payload",
),
);
assert.notStrictEqual(err.cause?.values, undefined);
assert.equal(err.cause.values.length, 2);
const [jsonError, yamlError] = err.cause.values;
assert.ok(jsonError instanceof SyntaxError);
assert.ok(yamlError instanceof YAMLException);
} else {
assert.fail(err);
}
}
});
});
describe("payload file", async () => {
it("parses complete YAML from the input payload file", async () => {
mocks.inputs = {
...mocks.inputs,
"payload-file-path": join(FIXTURES, "payload.yaml"),
};
const config = new Config(mocks.core);
const expected = {
message: "drink water",
channel: "C6H12O6H2O2",
};
assert.deepEqual(config.content.values, expected);
});
it("parses complete YML from the input payload file", async () => {
mocks.inputs = {
...mocks.inputs,
"payload-file-path": join(FIXTURES, "payload.yml"),
};
const config = new Config(mocks.core);
const expected = {
message: "drink coffee",
channel: "C0FFEEEEEEEE",
};
assert.deepEqual(config.content.values, expected);
});
it("parses complete JSON from the input payload file", async () => {
mocks.inputs = {
...mocks.inputs,
"payload-file-path": join(FIXTURES, "payload.json"),
};
const config = new Config(mocks.core);
const expected = {
message: "drink water",
channel: "C6H12O6H2O2",
};
assert.deepEqual(config.content.values, expected);
});
it("templatizes variables requires configuration", async () => {
mocks.inputs = {
...mocks.inputs,
"payload-file-path": join(FIXTURES, "payload-with-github-vars.json"),
};
const config = new Config(mocks.core);
// biome-ignore-start lint/suspicious/noTemplateCurlyInString: GitHub Action YAML variable syntax
const expected = {
message: "this matches an existing variable: ${{ github.apiUrl }}",
channel: "C0123456789",
};
// biome-ignore-end lint/suspicious/noTemplateCurlyInString: https://docs.github.com/en/actions/how-tos/writing-workflows/choosing-what-your-workflow-does/store-information-in-variables#using-contexts-to-access-variable-values
assert.deepEqual(config.content.values, expected);
});
it("templatizes variables with matching variables", async () => {
mocks.inputs = {
...mocks.inputs,
"payload-file-path": join(FIXTURES, "payload-with-env-vars.json"),
};
mocks.booleanInputs = {
...mocks.booleanInputs,
"payload-templated": true,
};
process.env.DETAILS = `
-fri
-sat
-sun`;
process.env.NUMBER = 12;
const config = new Config(mocks.core);
process.env.DETAILS = undefined;
process.env.NUMBER = undefined;
const expected = {
channel: "C0123456789",
reply_broadcast: false,
message: "Served 12 items",
blocks: [
{
type: "section",
text: {
type: "mrkdwn",
text: "Served 12 items on: \n-fri\n-sat\n-sun",
},
},
{
type: "divider",
},
{
type: "section",
block_id: "selector",
text: {
type: "mrkdwn",
text: "Send feedback",
},
accessory: {
action_id: "response",
type: "multi_static_select",
placeholder: {
type: "plain_text",
text: "Select URL",
},
options: [
{
text: {
type: "plain_text",
text: "https://api.github.com",
},
value: "api",
},
{
text: {
type: "plain_text",
text: "https://github.com",
},
value: "server",
},
{
text: {
type: "plain_text",
text: "https://api.github.com/graphql",
},
value: "graphql",
},
],
},
},
],
};
assert.deepEqual(config.content.values, expected);
});
/**
* @see {@link https://github.com/slackapi/slack-github-action/issues/203}
*/
it("templatizes variables with missing variables", async () => {
mocks.inputs = {
...mocks.inputs,
"payload-file-path": join(FIXTURES, "payload-with-missing-var.json"),
};
mocks.booleanInputs = {
...mocks.booleanInputs,
"payload-templated": true,
};
const config = new Config(mocks.core);
const expected = {
message: "What makes ??? a secret",
};
assert.deepEqual(config.content.values, expected);
});
it("fails if no payload file is provided in the input", async () => {
/**
* @type {Config}
*/
const config = {
core: mocks.core,
inputs: {
payload: "LGTM",
},
};
try {
new Content().getContentPayloadFilePath(config);
assert.fail("Failed to throw for the wrong payload type");
} catch (err) {
if (err instanceof SlackError) {
assert.ok(
err.message.includes("Invalid input! No payload found for content"),
);
} else {
assert.fail(err);
}
}
});
it("fails to parse a file path that does not exist", async () => {
mocks.inputs = {
...mocks.inputs,
"payload-file-path": join(FIXTURES, "nonexistent.json"),
};
try {
await send(mocks.core);
assert.fail("Failed to throw for nonexistent files");
} catch (err) {
if (err instanceof SlackError) {
assert.ok(
err.message.includes(
"Invalid input! Failed to parse contents of the provided payload file",
),
);
} else {
assert.fail(err);
}
}
});
it("fails to parse a file with an unknown extension", async () => {
mocks.inputs = {
...mocks.inputs,
"payload-file-path": join(FIXTURES, "payload.yaml.md"),
};
try {
await send(mocks.core);
assert.fail("Failed to throw for an unknown extension");
} catch (err) {
if (err instanceof SlackError) {
assert.ok(
err.message.includes(
"Invalid input! Failed to parse contents of the provided payload file",
),
);
assert.notStrictEqual(err.cause?.values, undefined);
assert.equal(err.cause.values.length, 1);
assert.ok(
err.cause.values[0].message.includes(
"Invalid input! Failed to parse file extension",
),
);
} else {
assert.fail(err);
}
}
});
it("fails if invalid JSON exists in the input payload", async () => {
mocks.inputs = {
...mocks.inputs,
"payload-file-path": join(FIXTURES, "payload-invalid.json"),
};
try {
await send(mocks.core);
assert.fail("Failed to throw for invalid JSON");
} catch (err) {
if (err instanceof SlackError) {
assert.ok(
err.message.includes(
"Invalid input! Failed to parse contents of the provided payload file",
),
);
assert.notStrictEqual(err.cause?.values, undefined);
assert.equal(err.cause.values.length, 1);
assert.ok(err.cause.values[0] instanceof SyntaxError);
} else {
assert.fail(err);
}
}
});
it("fails if invalid YAML exists in the input payload", async () => {
mocks.inputs = {
...mocks.inputs,
"payload-file-path": join(FIXTURES, "payload-invalid.yaml"),
};
try {
await send(mocks.core);
assert.fail("Failed to throw for invalid YAML");
} catch (err) {
if (err instanceof SlackError) {
assert.ok(
err.message.includes(
"Invalid input! Failed to parse contents of the provided payload file",
),
);
assert.notStrictEqual(err.cause?.values, undefined);
assert.equal(err.cause.values.length, 1);
assert.ok(err.cause.values[0] instanceof YAMLException);
} else {
assert.fail(err);
}
}
});
});
});