mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-03 03:55:26 +03:00
e170f293e3
no issue - The sleep method has been used in 8 modules reimplementing the same thing over and over again. It's usually a sign of async event processing outside of the request/response loop. It's good to have a single point of implementation for a "hack" like this, so we could track it easier and address the even processing delay in a more optimal way centrally if it ever becomes a bottleneck
241 lines
9.1 KiB
JavaScript
241 lines
9.1 KiB
JavaScript
require('should');
|
|
const {agentProvider, fixtureManager, mockManager, sleep} = require('../../utils/e2e-framework');
|
|
const moment = require('moment');
|
|
const ObjectId = require('bson-objectid').default;
|
|
const models = require('../../../core/server/models');
|
|
const sinon = require('sinon');
|
|
const assert = require('assert');
|
|
let agent;
|
|
|
|
async function createPublishedPostEmail() {
|
|
const post = {
|
|
title: 'A random test post',
|
|
status: 'draft',
|
|
feature_image_alt: 'Testing sending',
|
|
feature_image_caption: 'Testing <b>feature image caption</b>',
|
|
created_at: moment().subtract(2, 'days').toISOString(),
|
|
updated_at: moment().subtract(2, 'days').toISOString(),
|
|
created_by: ObjectId().toHexString(),
|
|
updated_by: ObjectId().toHexString()
|
|
};
|
|
|
|
const res = await agent.post('posts/')
|
|
.body({posts: [post]})
|
|
.expectStatus(201);
|
|
|
|
const id = res.body.posts[0].id;
|
|
|
|
const updatedPost = {
|
|
status: 'published',
|
|
updated_at: res.body.posts[0].updated_at
|
|
};
|
|
|
|
const newsletterSlug = fixtureManager.get('newsletters', 0).slug;
|
|
await agent.put(`posts/${id}/?newsletter=${newsletterSlug}`)
|
|
.body({posts: [updatedPost]})
|
|
.expectStatus(200);
|
|
|
|
const emailModel = await models.Email.findOne({
|
|
post_id: id
|
|
});
|
|
should.exist(emailModel);
|
|
|
|
return emailModel;
|
|
}
|
|
|
|
describe('MEGA', function () {
|
|
let _sendEmailJob;
|
|
let _mailgunClient;
|
|
let frontendAgent;
|
|
|
|
beforeEach(function () {
|
|
mockManager.mockLabsDisabled('emailStability');
|
|
});
|
|
|
|
afterEach(function () {
|
|
mockManager.restore();
|
|
});
|
|
|
|
describe('sendEmailJob', function () {
|
|
before(async function () {
|
|
agent = await agentProvider.getAdminAPIAgent();
|
|
await fixtureManager.init('newsletters', 'members:newsletters');
|
|
await agent.loginAsOwner();
|
|
_sendEmailJob = require('../../../core/server/services/mega/mega')._sendEmailJob;
|
|
_mailgunClient = require('../../../core/server/services/bulk-email')._mailgunClient;
|
|
});
|
|
|
|
it('Can send a scheduled post email', async function () {
|
|
sinon.stub(_mailgunClient, 'getInstance').returns({});
|
|
sinon.stub(_mailgunClient, 'send').callsFake(async () => {
|
|
return {
|
|
id: 'stubbed-email-id'
|
|
};
|
|
});
|
|
|
|
// Prepare a post and email model
|
|
const emailModel = await createPublishedPostEmail();
|
|
|
|
// Launch email job
|
|
await _sendEmailJob({emailId: emailModel.id, options: {}});
|
|
|
|
await emailModel.refresh();
|
|
emailModel.get('status').should.eql('submitted');
|
|
});
|
|
|
|
it('Protects the email job from being run multiple times at the same time', async function () {
|
|
sinon.stub(_mailgunClient, 'getInstance').returns({});
|
|
sinon.stub(_mailgunClient, 'send').callsFake(async () => {
|
|
return {
|
|
id: 'stubbed-email-id'
|
|
};
|
|
});
|
|
|
|
// Prepare a post and email model
|
|
const emailModel = await createPublishedPostEmail();
|
|
|
|
// Launch a lot of email jobs in the hope to mimic a possible race condition
|
|
const promises = [];
|
|
for (let i = 0; i < 100; i++) {
|
|
promises.push(_sendEmailJob({emailId: emailModel.id, options: {}}));
|
|
}
|
|
await Promise.all(promises);
|
|
|
|
await emailModel.refresh();
|
|
assert.equal(emailModel.get('status'), 'submitted');
|
|
|
|
const batchCount = await emailModel.related('emailBatches').count('id');
|
|
assert.equal(batchCount, 1, 'Should only have created one batch');
|
|
});
|
|
|
|
it('Can handle a failed post email', async function () {
|
|
sinon.stub(_mailgunClient, 'getInstance').returns({});
|
|
sinon.stub(_mailgunClient, 'send').callsFake(async () => {
|
|
throw new Error('Failed to send');
|
|
});
|
|
|
|
// Prepare a post and email model
|
|
const emailModel = await createPublishedPostEmail();
|
|
|
|
// Launch email job
|
|
await _sendEmailJob({emailId: emailModel.id, options: {}});
|
|
|
|
await emailModel.refresh();
|
|
emailModel.get('status').should.eql('failed');
|
|
});
|
|
});
|
|
|
|
/**
|
|
* This is one full E2E test that tests if the tracked links are valid and are working.
|
|
* More detailed tests don't have to cover the whole flow. But this test is useful because it also tests if the member uuids are correctly added in every link
|
|
* + it tests if all the pieces glue together correctly
|
|
*/
|
|
describe('Link click tracking', function () {
|
|
before(async function () {
|
|
const agents = await agentProvider.getAgentsWithFrontend();
|
|
agent = agents.adminAgent;
|
|
frontendAgent = agents.frontendAgent;
|
|
|
|
await fixtureManager.init('newsletters', 'members:newsletters');
|
|
await agent.loginAsOwner();
|
|
_sendEmailJob = require('../../../core/server/services/mega/mega')._sendEmailJob;
|
|
_mailgunClient = require('../../../core/server/services/bulk-email')._mailgunClient;
|
|
});
|
|
|
|
it('Tracks all the links in an email', async function () {
|
|
const linkRedirectService = require('../../../core/server/services/link-redirection');
|
|
const linkRedirectRepository = linkRedirectService.linkRedirectRepository;
|
|
|
|
const linkTrackingService = require('../../../core/server/services/link-tracking');
|
|
const linkClickRepository = linkTrackingService.linkClickRepository;
|
|
|
|
sinon.stub(_mailgunClient, 'getInstance').returns({});
|
|
const sendStub = sinon.stub(_mailgunClient, 'send');
|
|
|
|
sendStub.callsFake(async () => {
|
|
return {
|
|
id: 'stubbed-email-id'
|
|
};
|
|
});
|
|
|
|
// Prepare a post and email model
|
|
const emailModel = await createPublishedPostEmail();
|
|
|
|
// Launch email job
|
|
await _sendEmailJob({emailId: emailModel.id, options: {}});
|
|
|
|
await emailModel.refresh();
|
|
emailModel.get('status').should.eql('submitted');
|
|
|
|
// Get email data (first argument to sendStub call)
|
|
const emailData = sendStub.args[0][0];
|
|
const recipientData = sendStub.args[0][1];
|
|
const replacements = sendStub.args[0][2];
|
|
|
|
const recipient = recipientData[Object.keys(recipientData)[0]];
|
|
|
|
// Do the actual replacements for the first member, so we don't have to worry about them anymore
|
|
replacements.forEach((replacement) => {
|
|
emailData[replacement.format] = emailData[replacement.format].replace(
|
|
replacement.regexp,
|
|
recipient[replacement.id]
|
|
);
|
|
|
|
// Also force Mailgun format
|
|
emailData[replacement.format] = emailData[replacement.format].replace(
|
|
new RegExp(`%recipient.${replacement.id}%`, 'g'),
|
|
recipient[replacement.id]
|
|
);
|
|
});
|
|
|
|
const html = emailData.html;
|
|
|
|
const memberUuid = recipient.unique_id;
|
|
|
|
// Test if all links are replaced and contain the member id
|
|
const cheerio = require('cheerio');
|
|
const $ = cheerio.load(html);
|
|
|
|
const exclude = '%recipient.unsubscribe_url%';
|
|
let firstLink;
|
|
|
|
for (const el of $('a').toArray()) {
|
|
const href = $(el).attr('href');
|
|
|
|
if (href === exclude) {
|
|
continue;
|
|
}
|
|
|
|
// Check if the link is a tracked link
|
|
assert(href.includes('?m=' + memberUuid), href + ' is not tracked');
|
|
|
|
// Check if this link is also present in the plaintext version (with the right replacements)
|
|
assert(emailData.plaintext.includes(href), href + ' is not present in the plaintext version');
|
|
|
|
if (!firstLink) {
|
|
firstLink = new URL(href);
|
|
}
|
|
}
|
|
|
|
const links = await linkRedirectRepository.getAll({post_id: emailModel.get('post_id')});
|
|
const link = links.find(l => l.from.pathname === firstLink.pathname);
|
|
assert(link, 'Link model not created');
|
|
|
|
// Mimic a click on a link
|
|
const path = firstLink.pathname + firstLink.search;
|
|
await frontendAgent.get(path)
|
|
.expect('Location', link.to.href)
|
|
.expect(302);
|
|
|
|
// Since this is all event based, we need to wait a coulple of ms
|
|
// in the future we should wait for all dispatched events to be completed.
|
|
await sleep(200);
|
|
|
|
// Check if click was tracked and associated with the correct member
|
|
const member = await models.Member.findOne({uuid: memberUuid});
|
|
const clickEvent = await linkClickRepository.getAll({member_id: member.id, link_id: link.link_id.toHexString()});
|
|
assert(clickEvent.length === 1, 'Click event was not tracked');
|
|
});
|
|
});
|
|
});
|