Adding linting and unit tests.

This commit is contained in:
Filip Maj
2021-11-24 16:30:07 -05:00
parent 6cea2453e4
commit 51e7358c91
11 changed files with 4346 additions and 596 deletions
+2
View File
@@ -0,0 +1,2 @@
node_modules
dist
+132
View File
@@ -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
View File
@@ -1,2 +1,3 @@
node_modules
.DS_Store
.nyc_output
+3
View File
@@ -0,0 +1,3 @@
{
"timeout": 3000
}
-95
View File
@@ -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);
}
+3978 -497
View File
File diff suppressed because it is too large Load Diff
+16 -4
View File
@@ -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"
}
}
}
+4
View File
@@ -0,0 +1,4 @@
const core = require('@actions/core');
const slackSend = require('./slack-send');
slackSend(core);
+95
View File
@@ -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);
}
};
+15
View File
@@ -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,
},
};
+100
View File
@@ -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.');
});
});
});
});