Ghost/ghost/slack-notifications/test/SlackNotifications.test.js
Aileen Booker 2f57e95a5d
Slack notifications service for Milestones behind flag (#16281)
refs
https://www.notion.so/ghost/Marketing-Milestone-email-campaigns-1d2c9dee3cfa4029863edb16092ad5c4?pvs=4

- Added a `slack-notifications` repository which handles sending Slack
messages to a URL as defined in our Ghost(Pro) config (also includes a
global switch to disable the feature if needed) and listens to
`MilestoneCreatedEvents`.
- Added a `slack-notification` service which listens to the events on
boot.
- In order to have access to further information such as the reason why
a Milestone email hasn't been sent, or the current ARR or Member value
as comparison to the achieved milestone, I added a `meta` object to the
`MilestoneCreatedEvent` which then gets accessible by the event
subscriber. This avoid doing further requests to the DB as we need to
have this information in relation to the event occurred.

---------

Co-authored-by: Fabien "egg" O'Carroll <fabien@allou.is>
2023-02-17 12:59:18 +02:00

276 lines
10 KiB
JavaScript

const assert = require('assert');
const sinon = require('sinon');
const SlackNotifications = require('../lib/SlackNotifications');
const nock = require('nock');
const ObjectId = require('bson-objectid').default;
const got = require('got');
const ghostVersion = require('@tryghost/version');
describe('SlackNotifications', function () {
let slackNotifications;
let loggingErrorStub;
beforeEach(function () {
loggingErrorStub = sinon.stub();
slackNotifications = new SlackNotifications({
logging: {
warn: () => {},
error: loggingErrorStub
},
siteUrl: 'https://ghost.example',
webhookUrl: 'https://slack-webhook.example'
});
nock('https://slack-webhook.example')
.post('/')
.reply(200, {message: 'success'});
});
afterEach(function () {
sinon.restore();
nock.cleanAll();
});
describe('notifyMilestoneReceived', function () {
let sendStub;
beforeEach(function () {
sendStub = slackNotifications.send = sinon.stub().resolves();
});
afterEach(function () {
sinon.restore();
});
it('Sends a notification to Slack for achieved ARR Milestone - no meta', async function () {
await slackNotifications.notifyMilestoneReceived({
milestone: {
id: ObjectId().toHexString(),
name: 'arr-1000-usd',
type: 'arr',
createdAt: '2023-02-15T00:00:00.000Z',
emailSentAt: '2023-02-15T00:00:00.000Z',
value: 1000,
currency: 'gbp'
}
});
const expectedResult = {
unfurl_links: false,
username: 'Ghost Milestone Service',
attachments: [{
color: '#36a64f',
blocks: [
{
type: 'header',
text: {
type: 'plain_text',
text: ':tada: ARR Milestone £1,000.00 reached!',
emoji: true
}
},
{
type: 'section',
text: {
type: 'mrkdwn',
text: 'New *ARR Milestone* achieved for <https://ghost.example|https://ghost.example>'
}
},
{
type: 'divider'
},
{
type: 'section',
fields: [
{
type: 'mrkdwn',
text: '*Milestone:*\n£1,000.00'
}
]
},
{
type: 'section',
text: {
type: 'mrkdwn',
text: '*Email sent:*\n15 Feb 2023'
}
}
]
}]
};
assert(sendStub.calledOnce === true);
assert(sendStub.calledWith(expectedResult, 'https://slack-webhook.example') === true);
});
it('Sends a notification to Slack for achieved Members Milestone and shows reason when imported members', async function () {
await slackNotifications.notifyMilestoneReceived({
milestone: {
id: ObjectId().toHexString(),
name: 'members-50000',
type: 'members',
createdAt: null,
emailSentAt: null,
value: 50000
},
meta: {
currentMembers: 59857,
reason: 'import'
}
});
const expectedResult = {
unfurl_links: false,
username: 'Ghost Milestone Service',
attachments: [{
color: '#36a64f',
blocks: [
{
type: 'header',
text: {
type: 'plain_text',
text: ':tada: Members Milestone 50,000 reached!',
emoji: true
}
},
{
type: 'section',
text: {
type: 'mrkdwn',
text: 'New *Members Milestone* achieved for <https://ghost.example|https://ghost.example>'
}
},
{
type: 'divider'
},
{
type: 'section',
fields: [
{
type: 'mrkdwn',
text: '*Milestone:*\n50,000'
},
{
type: 'mrkdwn',
text: '*Current Members:*\n59,857'
}
]
},
{
type: 'section',
text: {
type: 'mrkdwn',
text: '*Email sent:*\nno / has imported members'
}
}
]
}]
};
assert(sendStub.calledOnce === true);
assert(sendStub.calledWith(expectedResult, 'https://slack-webhook.example') === true);
});
it('Shows the correct reason for email not send when last email was too recent', async function () {
await slackNotifications.notifyMilestoneReceived({
milestone: {
id: ObjectId().toHexString(),
name: 'arr-1000-eur',
type: 'arr',
currency: 'eur',
createdAt: '2023-02-15T00:00:00.000Z',
emailSentAt: null,
value: 1000
},
meta: {
currentARR: 1005,
reason: 'email'
}
});
const expectedResult = {
unfurl_links: false,
username: 'Ghost Milestone Service',
attachments: [{
color: '#36a64f',
blocks: [
{
type: 'header',
text: {
type: 'plain_text',
text: ':tada: ARR Milestone €1,000.00 reached!',
emoji: true
}
},
{
type: 'section',
text: {
type: 'mrkdwn',
text: 'New *ARR Milestone* achieved for <https://ghost.example|https://ghost.example>'
}
},
{
type: 'divider'
},
{
type: 'section',
fields: [
{
type: 'mrkdwn',
text: '*Milestone:*\n€1,000.00'
},
{
type: 'mrkdwn',
text: '*Current ARR:*\n€1,005.00'
}
]
},
{
type: 'section',
text: {
type: 'mrkdwn',
text: '*Email sent:*\nno / last email too recent'
}
}
]
}]
};
assert(sendStub.calledOnce === true);
assert(sendStub.calledWith(expectedResult, 'https://slack-webhook.example') === true);
});
});
describe('send', function () {
it('Sends with correct requestOptions', async function () {
const gotStub = sinon.stub(got, 'post').resolves();
sinon.stub(ghostVersion, 'original').value('5.0.0');
const expectedRequestOptions = [
'https://slack-webhook.com',
{
body: '{"data":"test"}',
headers: {'user-agent': 'Ghost/5.0.0 (https://github.com/TryGhost/Ghost)'}
}
];
await slackNotifications.send({data: 'test'}, 'https://slack-webhook.com');
assert(loggingErrorStub.callCount === 0);
assert(gotStub.calledOnce === true);
const gotStubArgs = gotStub.getCall(0).args;
assert.deepEqual(gotStubArgs, expectedRequestOptions);
});
it('Throws when invalid URL is passed', async function () {
await slackNotifications.send({}, 'https://invalid-url');
assert(loggingErrorStub.callCount === 1);
});
it('Throws when no URL is passed', async function () {
await slackNotifications.send({}, null);
assert(loggingErrorStub.callCount === 1);
});
});
});