Adding linting and unit tests.
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
node_modules
|
||||
dist
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
//
|
||||
// SlackAPI JavaScript style
|
||||
// ---
|
||||
// This style helps maintainers enforce safe and consistent programming practices in this project. It is not meant to be
|
||||
// comprehensive on its own or vastly different from existing styles. The goal is to inherit and aggregate as many of
|
||||
// the communities' recommended styles for the technologies used as we can. When, and only when, we have a stated need
|
||||
// to differentiate, we add more rules (or modify options). Therefore, the fewer rules directly defined in this file,
|
||||
// the better.
|
||||
|
||||
module.exports = {
|
||||
// This is a root of the project, ESLint should not look through parent directories to find more config
|
||||
root: true,
|
||||
|
||||
ignorePatterns: [
|
||||
// Ignore all build outputs and artifacts (node_modules, dotfiles, and dot directories are implicitly ignored)
|
||||
'/dist',
|
||||
'/coverage',
|
||||
],
|
||||
|
||||
// These environments contain lists of global variables which are allowed to be accessed
|
||||
env: {
|
||||
// According to https://node.green, the target node version (v10) supports all important ES2018 features. But es2018
|
||||
// is not an option since it presumably doesn't introduce any new globals over ES2017.
|
||||
es2017: true,
|
||||
node: true,
|
||||
},
|
||||
|
||||
extends: [
|
||||
// ESLint's recommended built-in rules: https://eslint.org/docs/rules/
|
||||
'eslint:recommended',
|
||||
|
||||
// Node plugin's recommended rules: https://github.com/mysticatea/eslint-plugin-node
|
||||
'plugin:node/recommended',
|
||||
|
||||
// AirBnB style guide (without React) rules: https://github.com/airbnb/javascript.
|
||||
'airbnb-base',
|
||||
|
||||
// JSDoc plugin's recommended rules
|
||||
'plugin:jsdoc/recommended',
|
||||
],
|
||||
|
||||
rules: {
|
||||
// JavaScript rules
|
||||
// ---
|
||||
// The top level of this configuration contains rules which apply to JavaScript (and will also be inherited for
|
||||
// TypeScript). This section does not contain rules meant to override options or disable rules in the base
|
||||
// configurations (ESLint, Node, AirBnb). Those rules are added in the final override.
|
||||
|
||||
// Eliminate tabs to standardize on spaces for indentation. If you want to use tabs for something other than
|
||||
// indentation, you may need to turn this rule off using an inline config comments.
|
||||
'no-tabs': 'error',
|
||||
|
||||
// Bans use of comma as an operator because it can obscure side effects and is often an accident.
|
||||
'no-sequences': 'error',
|
||||
|
||||
// This repo uses console.log, which is fine to do in GitHub actions
|
||||
'no-console': 'off',
|
||||
|
||||
// Disallow the use of process.exit()
|
||||
'node/no-process-exit': 'error',
|
||||
|
||||
// Allow safe references to functions before the declaration. Overrides AirBnB config. Not located in the override
|
||||
// section below because a distinct override is necessary in TypeScript files.
|
||||
'no-use-before-define': ['error', 'nofunc'],
|
||||
},
|
||||
|
||||
overrides: [
|
||||
{
|
||||
files: ['**/*.js'],
|
||||
rules: {
|
||||
// Override rules
|
||||
// ---
|
||||
// This level of this configuration contains rules which override options or disable rules in the base
|
||||
// configurations in JavaScript.
|
||||
|
||||
// Increase the max line length to 120. The rest of this setting is copied from the AirBnB config.
|
||||
'max-len': ['error', 120, 2, {
|
||||
ignoreUrls: true,
|
||||
ignoreComments: false,
|
||||
ignoreRegExpLiterals: true,
|
||||
ignoreStrings: true,
|
||||
ignoreTemplateLiterals: true,
|
||||
}],
|
||||
|
||||
// Restrict the use of backticks to declare a normal string. Template literals should only be used when the
|
||||
// template string contains placeholders. The rest of this setting is copied from the AirBnb config.
|
||||
quotes: ['error', 'single', { avoidEscape: true, allowTemplateLiterals: false }],
|
||||
|
||||
// the server side Slack API uses snake_case for parameters often
|
||||
// for mocking and override support, we need to allow snake_case
|
||||
// Allow leading underscores for parameter names, which is used to acknowledge unused variables in TypeScript.
|
||||
// Also, enforce camelCase naming for variables. Ideally, the leading underscore could be restricted to only
|
||||
// unused parameter names, but this rule isn't capable of knowing when a variable is unused. The camelcase and
|
||||
// no-underscore-dangle rules are replaced with the naming-convention rule because this single rule can serve
|
||||
// both purposes, and it works fine on non-TypeScript code.
|
||||
camelcase: 'off',
|
||||
'no-underscore-dangle': 'off',
|
||||
|
||||
// Remove the minProperties option for enforcing line breaks between braces. The AirBnB config sets this to 4,
|
||||
// which is arbitrary and not backed by anything specific in the style guide. If we just remove it, we can
|
||||
// rely on the max-len rule to determine if the line is too long and then enforce line breaks. Overrides AirBnB
|
||||
// styles.
|
||||
'object-curly-newline': ['error', { multiline: true, consistent: true }],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['src/test/*.js'],
|
||||
rules: {
|
||||
// Test-specific rules
|
||||
// ---
|
||||
// Rules that only apply to JavaScript _test_ source files
|
||||
|
||||
// With Mocha as a test framework, it is sometimes helpful to assign
|
||||
// shared state to Mocha's Context object, for example in setup and
|
||||
// teardown test methods. Assigning stub/mock objects to the Context
|
||||
// object via `this` is a common pattern in Mocha. As such, using
|
||||
// `function` over the the arrow notation binds `this` appropriately and
|
||||
// should be used in tests. So: we turn off the prefer-arrow-callback
|
||||
// rule.
|
||||
// See https://github.com/slackapi/bolt-js/pull/1012#pullrequestreview-711232738
|
||||
// for a case of arrow-vs-function syntax coming up for the team
|
||||
'prefer-arrow-callback': 'off',
|
||||
|
||||
// Using ununamed functions (e.g., null logger) in tests is fine
|
||||
'func-names': 'off',
|
||||
// In tests, don't force constructing a Symbol with a descriptor, as
|
||||
// it's probably just for tests
|
||||
'symbol-description': 'off',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,2 +1,3 @@
|
||||
node_modules
|
||||
.DS_Store
|
||||
.nyc_output
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"timeout": 3000
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
const core = require('@actions/core');
|
||||
const github = require('@actions/github');
|
||||
const { WebClient } = require('@slack/web-api');
|
||||
const flatten = require('flat');
|
||||
const axios = require('axios');
|
||||
|
||||
const SLACK_WEBHOOK_TYPES = {
|
||||
WORKFLOW_TRIGGER: 'WORKFLOW_TRIGGER',
|
||||
INCOMING_WEBHOOK: 'INCOMING_WEBHOOK'
|
||||
}
|
||||
|
||||
try {
|
||||
const botToken = process.env.SLACK_BOT_TOKEN;
|
||||
const webhookUrl = process.env.SLACK_WEBHOOK_URL;
|
||||
let webhookType = SLACK_WEBHOOK_TYPES.WORKFLOW_TRIGGER;
|
||||
|
||||
if(process.env.SLACK_WEBHOOK_TYPE) {
|
||||
// The default type is for Workflow Builder triggers. If you want to use this action for Incoming Webhooks, use the corresponding type instead.
|
||||
webhookType = process.env.SLACK_WEBHOOK_TYPE.toUpperCase()
|
||||
}
|
||||
|
||||
let payload = core.getInput('payload');
|
||||
|
||||
if (botToken === undefined && webhookUrl === undefined) {
|
||||
throw 'Need to provide at least one botToken or webhookUrl'
|
||||
}
|
||||
|
||||
if (payload) {
|
||||
try {
|
||||
// confirm it is valid json
|
||||
payload = JSON.parse(payload);
|
||||
} catch (e) {
|
||||
// passed in payload wasn't valid json
|
||||
console.error("passed in payload was invalid JSON")
|
||||
throw 'Need to provide valid JSON payload'
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof botToken !== 'undefined' && botToken.length > 0) {
|
||||
const message = core.getInput('slack-message');
|
||||
const channelId = core.getInput('channel-id');
|
||||
const web = new WebClient(botToken);
|
||||
|
||||
|
||||
if (channelId.length > 0 && (message.length > 0 || payload)) {
|
||||
// post message
|
||||
web.chat.postMessage({ channel: channelId, text: message, ...(payload || {}) });
|
||||
} else {
|
||||
console.log('missing either channel-id, slack-message or payload! Did not send a message via chat.postMessage with botToken');
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof webhookUrl !== 'undefined' && webhookUrl.length > 0) {
|
||||
|
||||
if (!payload) {
|
||||
// No Payload was passed in
|
||||
console.log('no custom payload was passed in, using default payload that triggered the GitHub Action')
|
||||
// Get the JSON webhook payload for the event that triggered the workflow
|
||||
payload = github.context.payload;
|
||||
}
|
||||
|
||||
if (webhookType === SLACK_WEBHOOK_TYPES.WORKFLOW_TRIGGER) {
|
||||
// flatten JSON payload (no nested attributes)
|
||||
const flatPayload = flatten(payload);
|
||||
|
||||
// workflow builder requires values to be strings
|
||||
// iterate over every value and convert it to string
|
||||
Object.keys(flatPayload).forEach((key) => {
|
||||
flatPayload[key] = '' + flatPayload[key];
|
||||
})
|
||||
|
||||
payload = flatPayload;
|
||||
}
|
||||
|
||||
axios.post(webhookUrl, payload).then(response => {
|
||||
// Successful post!
|
||||
}).catch(err => {
|
||||
console.log("axios post failed, double check the payload being sent includes the keys Slack expects")
|
||||
console.log(payload);
|
||||
// console.log(err);
|
||||
|
||||
if (err.response) {
|
||||
core.setFailed(err.response.data);
|
||||
}
|
||||
|
||||
core.setFailed(err.message);
|
||||
})
|
||||
}
|
||||
|
||||
const time = (new Date()).toTimeString();
|
||||
core.setOutput("time", time);
|
||||
|
||||
} catch (error) {
|
||||
core.setFailed(error);
|
||||
}
|
||||
Generated
+3978
-497
File diff suppressed because it is too large
Load Diff
+16
-4
@@ -4,8 +4,10 @@
|
||||
"description": "The official slack github action. Use this to send data into your Slack workspace",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"build": "npx @vercel/ncc build index.js --license licenses.txt"
|
||||
"lint": "eslint .",
|
||||
"test:mocha": "nyc mocha --config .mocharc.json test/*-test.js",
|
||||
"test": "npm run lint && npm run test:mocha",
|
||||
"build": "npx @vercel/ncc build src/index.js --license licenses.txt"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -35,6 +37,16 @@
|
||||
"flat": "^5.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vercel/ncc": "^0.31.1"
|
||||
"@vercel/ncc": "^0.31.1",
|
||||
"chai": "^4.3.4",
|
||||
"eslint": "^8.3.0",
|
||||
"eslint-config-airbnb-base": "^15.0.0",
|
||||
"eslint-plugin-import": "^2.25.3",
|
||||
"eslint-plugin-jsdoc": "^37.0.3",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"mocha": "^9.1.3",
|
||||
"nyc": "^15.1.0",
|
||||
"rewiremock": "^3.14.3",
|
||||
"sinon": "^12.0.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
const core = require('@actions/core');
|
||||
const slackSend = require('./slack-send');
|
||||
|
||||
slackSend(core);
|
||||
@@ -0,0 +1,95 @@
|
||||
const github = require('@actions/github');
|
||||
const { WebClient } = require('@slack/web-api');
|
||||
const flatten = require('flat');
|
||||
const axios = require('axios');
|
||||
|
||||
const SLACK_WEBHOOK_TYPES = {
|
||||
WORKFLOW_TRIGGER: 'WORKFLOW_TRIGGER',
|
||||
INCOMING_WEBHOOK: 'INCOMING_WEBHOOK',
|
||||
};
|
||||
|
||||
module.exports = async function slackSend(core) {
|
||||
try {
|
||||
const botToken = process.env.SLACK_BOT_TOKEN;
|
||||
const webhookUrl = process.env.SLACK_WEBHOOK_URL;
|
||||
let webhookType = SLACK_WEBHOOK_TYPES.WORKFLOW_TRIGGER;
|
||||
|
||||
if (process.env.SLACK_WEBHOOK_TYPE) {
|
||||
// The default type is for Workflow Builder triggers. If you want to use this action for Incoming Webhooks, use
|
||||
// the corresponding type instead.
|
||||
webhookType = process.env.SLACK_WEBHOOK_TYPE.toUpperCase();
|
||||
}
|
||||
|
||||
if (botToken === undefined && webhookUrl === undefined) {
|
||||
throw new Error('Need to provide at least one botToken or webhookUrl');
|
||||
}
|
||||
|
||||
let payload = core.getInput('payload');
|
||||
|
||||
if (payload) {
|
||||
try {
|
||||
// confirm it is valid json
|
||||
payload = JSON.parse(payload);
|
||||
} catch (e) {
|
||||
// passed in payload wasn't valid json
|
||||
console.error('passed in payload was invalid JSON');
|
||||
throw new Error('Need to provide valid JSON payload');
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof botToken !== 'undefined' && botToken.length > 0) {
|
||||
const message = core.getInput('slack-message');
|
||||
const channelId = core.getInput('channel-id');
|
||||
const web = new WebClient(botToken);
|
||||
|
||||
if (channelId.length > 0 && (message.length > 0 || payload)) {
|
||||
// post message
|
||||
await web.chat.postMessage({ channel: channelId, text: message, ...(payload || {}) });
|
||||
} else {
|
||||
console.log('missing either channel-id, slack-message or payload! Did not send a message via chat.postMessage with botToken');
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof webhookUrl !== 'undefined' && webhookUrl.length > 0) {
|
||||
if (!payload) {
|
||||
// No Payload was passed in
|
||||
console.log('no custom payload was passed in, using default payload that triggered the GitHub Action');
|
||||
// Get the JSON webhook payload for the event that triggered the workflow
|
||||
payload = github.context.payload;
|
||||
}
|
||||
|
||||
if (webhookType === SLACK_WEBHOOK_TYPES.WORKFLOW_TRIGGER) {
|
||||
// flatten JSON payload (no nested attributes)
|
||||
const flatPayload = flatten(payload);
|
||||
|
||||
// workflow builder requires values to be strings
|
||||
// iterate over every value and convert it to string
|
||||
Object.keys(flatPayload).forEach((key) => {
|
||||
flatPayload[key] = `${flatPayload[key]}`;
|
||||
});
|
||||
|
||||
payload = flatPayload;
|
||||
}
|
||||
|
||||
try {
|
||||
await axios.post(webhookUrl, payload);
|
||||
} catch (err) {
|
||||
console.log('axios post failed, double check the payload being sent includes the keys Slack expects');
|
||||
console.log(payload);
|
||||
// console.log(err);
|
||||
|
||||
if (err.response) {
|
||||
core.setFailed(err.response.data);
|
||||
}
|
||||
|
||||
core.setFailed(err.message);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const time = (new Date()).toTimeString();
|
||||
core.setOutput('time', time);
|
||||
} catch (error) {
|
||||
core.setFailed(error);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
module.exports = {
|
||||
// These environments contain lists of global variables which are allowed to be accessed
|
||||
env: {
|
||||
// According to https://node.green, the target node version (v10) supports all important ES2018 features. But es2018
|
||||
// is not an option since it presumably doesn't introduce any new globals over ES2017.
|
||||
es2017: true,
|
||||
node: true,
|
||||
mocha: true,
|
||||
},
|
||||
rules: {
|
||||
// These rules dont like the use of devDependencies - which test code uses often.
|
||||
'node/no-unpublished-require': 0,
|
||||
'node/no-missing-require': 0,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,100 @@
|
||||
const { assert } = require('chai');
|
||||
const sinon = require('sinon');
|
||||
const core = require('@actions/core');
|
||||
const rewiremock = require('rewiremock/node');
|
||||
|
||||
const ChatStub = {
|
||||
postMessage: sinon.spy(),
|
||||
};
|
||||
/* eslint-disable-next-line global-require */
|
||||
rewiremock(() => require('@slack/web-api')).with({
|
||||
WebClient: class {
|
||||
constructor(token) {
|
||||
this.token = token;
|
||||
this.chat = ChatStub;
|
||||
}
|
||||
},
|
||||
});
|
||||
const AxiosMock = {
|
||||
post: sinon.stub().resolves(),
|
||||
};
|
||||
/* eslint-disable-next-line global-require */
|
||||
rewiremock(() => require('axios')).with(AxiosMock);
|
||||
rewiremock.enable();
|
||||
const slackSend = require('../src/slack-send');
|
||||
|
||||
rewiremock.disable();
|
||||
|
||||
const ORIG_TOKEN_VAR = process.env.SLACK_BOT_TOKEN;
|
||||
const ORIG_WEBHOOK_VAR = process.env.SLACK_WEBHOOK_URL;
|
||||
|
||||
describe('slack-send', () => {
|
||||
const fakeCore = sinon.stub(core);
|
||||
beforeEach(() => {
|
||||
sinon.reset();
|
||||
});
|
||||
after(() => {
|
||||
process.env.SLACK_BOT_TOKEN = ORIG_TOKEN_VAR;
|
||||
process.env.SLACK_WEBHOOK_URL = ORIG_WEBHOOK_VAR;
|
||||
});
|
||||
|
||||
it('should set an error if no webhook URL or token is provided', async () => {
|
||||
delete process.env.SLACK_BOT_TOKEN;
|
||||
delete process.env.SLACK_WEBHOOK_URL;
|
||||
await slackSend(fakeCore);
|
||||
assert.include(fakeCore.setFailed.lastCall.firstArg.message, 'Need to provide at least one botToken or webhook', 'Error set specifying what env vars need to be set.');
|
||||
});
|
||||
|
||||
describe('using a bot token', () => {
|
||||
beforeEach(() => {
|
||||
process.env.SLACK_BOT_TOKEN = 'xoxb-xxxxx';
|
||||
delete process.env.SLACK_WEBHOOK_URL;
|
||||
});
|
||||
describe('happy path', () => {
|
||||
it('should send a message using the postMessage API', async () => {
|
||||
fakeCore.getInput.withArgs('slack-message').returns('who let the dogs out?');
|
||||
fakeCore.getInput.withArgs('channel-id').returns('C123456');
|
||||
await slackSend(fakeCore);
|
||||
assert.equal(fakeCore.setOutput.lastCall.firstArg, 'time', 'Output name set to time');
|
||||
assert(fakeCore.setOutput.lastCall.lastArg.length > 0, 'Time output a non-zero-length string');
|
||||
const chatArgs = ChatStub.postMessage.lastCall.firstArg;
|
||||
assert.equal(chatArgs.channel, 'C123456', 'Correct channel provided to postMessage');
|
||||
assert.equal(chatArgs.text, 'who let the dogs out?', 'Correct message provided to postMessage');
|
||||
});
|
||||
});
|
||||
describe('sad path', () => {
|
||||
it('should set an error if payload cannot be JSON parsed', async () => {
|
||||
fakeCore.getInput.withArgs('payload').returns('{not-valid-json');
|
||||
await slackSend(fakeCore);
|
||||
assert.include(fakeCore.setFailed.lastCall.firstArg.message, 'Need to provide valid JSON', 'Error set specifying JSON was invalid.');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('using a webhook URL', () => {
|
||||
beforeEach(() => {
|
||||
process.env.SLACK_WEBHOOK_URL = 'https://someurl';
|
||||
delete process.env.SLACK_BOT_TOKEN;
|
||||
});
|
||||
describe('happy path', () => {
|
||||
const payload = {
|
||||
batman: 'robin',
|
||||
thor: 'loki',
|
||||
};
|
||||
beforeEach(() => {
|
||||
fakeCore.getInput.withArgs('payload').returns(JSON.stringify(payload));
|
||||
});
|
||||
it('should post the payload to the webhook URL', async () => {
|
||||
await slackSend(fakeCore);
|
||||
assert(AxiosMock.post.calledWith('https://someurl', payload));
|
||||
});
|
||||
});
|
||||
describe('sad path', () => {
|
||||
it('should set an error if the POST to the webhook fails without a response', async () => {
|
||||
AxiosMock.post.rejects(new Error('boom'));
|
||||
await slackSend(fakeCore);
|
||||
assert.include(fakeCore.setFailed.lastCall.firstArg, 'boom', 'Error set to whatever axios reports as error.');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user