mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-27 00:52:36 +03:00
Updated unit test coverage for email service (#15913)
refs https://github.com/TryGhost/Team/issues/2339 - adds unit test for email renderer, segmenter and sending service
This commit is contained in:
parent
c74857052f
commit
f9161876b3
@ -15,8 +15,8 @@ class EmailSegmenter {
|
||||
#membersRepository;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {object} dependencies
|
||||
*
|
||||
* @param {object} dependencies
|
||||
* @param {MembersRepository} dependencies.membersRepository
|
||||
*/
|
||||
constructor({
|
||||
@ -27,7 +27,7 @@ class EmailSegmenter {
|
||||
|
||||
getMemberFilterForSegment(newsletter, emailRecipientFilter, segment) {
|
||||
const filter = [`newsletters.id:${newsletter.id}`];
|
||||
|
||||
|
||||
switch (emailRecipientFilter) {
|
||||
case 'all':
|
||||
break;
|
||||
@ -39,7 +39,7 @@ class EmailSegmenter {
|
||||
filter.push(`(${emailRecipientFilter})`);
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
const visibility = newsletter.get('visibility');
|
||||
switch (visibility) {
|
||||
case 'members':
|
||||
@ -59,10 +59,10 @@ class EmailSegmenter {
|
||||
if (segment) {
|
||||
filter.push(`(${segment})`);
|
||||
}
|
||||
|
||||
|
||||
return filter.join('+');
|
||||
}
|
||||
|
||||
|
||||
async getMembersCount(newsletter, emailRecipientFilter, segment) {
|
||||
const filter = this.getMemberFilterForSegment(newsletter, emailRecipientFilter, segment);
|
||||
const {meta: {pagination: {total: membersCount}}} = await this.#membersRepository.list({filter});
|
||||
|
@ -89,4 +89,339 @@ describe('Email renderer', function () {
|
||||
assert.equal(replacements[2].getValue({name: ''}), '');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPost', function () {
|
||||
const emailRenderer = new EmailRenderer({
|
||||
urlUtils: {
|
||||
urlFor: () => 'http://example.com'
|
||||
}
|
||||
});
|
||||
|
||||
it('returns a post with correct subject from meta', function () {
|
||||
let post = {
|
||||
related: () => {
|
||||
return {
|
||||
get: () => {
|
||||
return 'Test Newsletter';
|
||||
}
|
||||
};
|
||||
},
|
||||
get: () => {
|
||||
return 'Sample Newsletter';
|
||||
}
|
||||
};
|
||||
let response = emailRenderer.getSubject(post);
|
||||
response.should.equal('Test Newsletter');
|
||||
});
|
||||
|
||||
it('returns a post with correct subject from title', function () {
|
||||
let post = {
|
||||
related: () => {
|
||||
return {
|
||||
get: () => {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
},
|
||||
get: () => {
|
||||
return 'Sample Newsletter';
|
||||
}
|
||||
};
|
||||
let response = emailRenderer.getSubject(post);
|
||||
response.should.equal('Sample Newsletter');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getFromAddress', function () {
|
||||
let emailRenderer = new EmailRenderer({
|
||||
settingsCache: {
|
||||
get: (key) => {
|
||||
if (key === 'title') {
|
||||
return 'Test Blog';
|
||||
}
|
||||
}
|
||||
},
|
||||
settingsHelpers: {
|
||||
getNoReplyAddress: () => {
|
||||
return 'reply@example.com';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('returns correct from address for newsletter', function () {
|
||||
let newsletter = {
|
||||
get: (key) => {
|
||||
if (key === 'sender_email') {
|
||||
return 'ghost@example.com';
|
||||
}
|
||||
|
||||
if (key === 'sender_name') {
|
||||
return 'Ghost';
|
||||
}
|
||||
}
|
||||
};
|
||||
let response = emailRenderer.getFromAddress({}, newsletter);
|
||||
response.should.equal('"Ghost" <ghost@example.com>');
|
||||
|
||||
newsletter = {
|
||||
get: (key) => {
|
||||
if (key === 'sender_email') {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (key === 'sender_name') {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
};
|
||||
response = emailRenderer.getFromAddress({}, newsletter);
|
||||
response.should.equal('"Test Blog" <reply@example.com>');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getReplyToAddress', function () {
|
||||
let emailRenderer = new EmailRenderer({
|
||||
settingsCache: {
|
||||
get: (key) => {
|
||||
if (key === 'title') {
|
||||
return 'Test Blog';
|
||||
}
|
||||
}
|
||||
},
|
||||
settingsHelpers: {
|
||||
getMembersSupportAddress: () => {
|
||||
return 'support@example.com';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('returns correct reply to address for newsletter', function () {
|
||||
let newsletter = {
|
||||
get: (key) => {
|
||||
if (key === 'sender_email') {
|
||||
return 'ghost@example.com';
|
||||
}
|
||||
|
||||
if (key === 'sender_name') {
|
||||
return 'Ghost';
|
||||
}
|
||||
|
||||
if (key === 'sender_reply_to') {
|
||||
return 'support';
|
||||
}
|
||||
}
|
||||
};
|
||||
let response = emailRenderer.getReplyToAddress({}, newsletter);
|
||||
response.should.equal('support@example.com');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSegments', function () {
|
||||
let emailRenderer = new EmailRenderer({
|
||||
renderers: {
|
||||
lexical: {
|
||||
render: () => {
|
||||
return '<p> Lexical Test</p>';
|
||||
}
|
||||
},
|
||||
mobiledoc: {
|
||||
render: () => {
|
||||
return '<p> Mobiledoc Test</p>';
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('returns correct empty segment for post', function () {
|
||||
let post = {
|
||||
url: '',
|
||||
get: (key) => {
|
||||
if (key === 'lexical') {
|
||||
return '{}';
|
||||
}
|
||||
}
|
||||
};
|
||||
let response = emailRenderer.getSegments(post);
|
||||
response.should.eql([null]);
|
||||
|
||||
post = {
|
||||
url: '',
|
||||
get: (key) => {
|
||||
if (key === 'mobiledoc') {
|
||||
return '{}';
|
||||
}
|
||||
}
|
||||
};
|
||||
response = emailRenderer.getSegments(post);
|
||||
response.should.eql([null]);
|
||||
});
|
||||
|
||||
it('returns correct segments for post with members only card', function () {
|
||||
emailRenderer = new EmailRenderer({
|
||||
renderers: {
|
||||
lexical: {
|
||||
render: () => {
|
||||
return '<p> Lexical Test <!--members-only--> members only section</p>';
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let post = {
|
||||
url: '',
|
||||
get: (key) => {
|
||||
if (key === 'lexical') {
|
||||
return '{}';
|
||||
}
|
||||
}
|
||||
};
|
||||
let response = emailRenderer.getSegments(post);
|
||||
response.should.eql(['status:free', 'status:-free']);
|
||||
});
|
||||
|
||||
it('returns correct segments for post with email card', function () {
|
||||
emailRenderer = new EmailRenderer({
|
||||
renderers: {
|
||||
lexical: {
|
||||
render: () => {
|
||||
return '<html> <div> Lexical Test </div> <div data-gh-segment="status:-free"> members only section</div> </html>';
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let post = {
|
||||
url: '',
|
||||
get: (key) => {
|
||||
if (key === 'lexical') {
|
||||
return '{}';
|
||||
}
|
||||
}
|
||||
};
|
||||
let response = emailRenderer.getSegments(post);
|
||||
response.should.eql(['status:-free']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('renderBody', function () {
|
||||
let emailRenderer = new EmailRenderer({
|
||||
audienceFeedbackService: {
|
||||
buildLink: () => {
|
||||
return new URL('http://example.com');
|
||||
}
|
||||
},
|
||||
urlUtils: {
|
||||
urlFor: () => {
|
||||
return 'http://icon.example.com';
|
||||
}
|
||||
},
|
||||
settingsCache: {
|
||||
get: (key) => {
|
||||
if (key === 'accent_color') {
|
||||
return '#ffffff';
|
||||
}
|
||||
if (key === 'timezone') {
|
||||
return 'Etc/UTC';
|
||||
}
|
||||
if (key === 'title') {
|
||||
return 'Test Blog';
|
||||
}
|
||||
if (key === 'icon') {
|
||||
return 'ICON';
|
||||
}
|
||||
}
|
||||
},
|
||||
getPostUrl: () => {
|
||||
return 'http://example.com';
|
||||
},
|
||||
renderers: {
|
||||
lexical: {
|
||||
render: () => {
|
||||
return '<p> Lexical Test</p>';
|
||||
}
|
||||
},
|
||||
mobiledoc: {
|
||||
render: () => {
|
||||
return '<p> Mobiledoc Test</p>';
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('returns correct empty segment for post', async function () {
|
||||
let post = {
|
||||
url: '',
|
||||
related: () => {
|
||||
return null;
|
||||
},
|
||||
get: (key) => {
|
||||
if (key === 'lexical') {
|
||||
return '{}';
|
||||
}
|
||||
|
||||
if (key === 'visibility') {
|
||||
return 'public';
|
||||
}
|
||||
|
||||
if (key === 'title') {
|
||||
return 'Test Post';
|
||||
}
|
||||
},
|
||||
getLazyRelation: () => {
|
||||
return {
|
||||
models: [{
|
||||
get: (key) => {
|
||||
if (key === 'name') {
|
||||
return 'Test Author';
|
||||
}
|
||||
}
|
||||
}]
|
||||
};
|
||||
}
|
||||
};
|
||||
let newsletter = {
|
||||
get: (key) => {
|
||||
if (key === 'header_image') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (key === 'name') {
|
||||
return 'Test Newsletter';
|
||||
}
|
||||
|
||||
if (key === 'badge') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (key === 'feedback_enabled') {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
let segment = null;
|
||||
let options = {};
|
||||
|
||||
let response = await emailRenderer.renderBody(
|
||||
post,
|
||||
newsletter,
|
||||
segment,
|
||||
options
|
||||
);
|
||||
|
||||
response.plaintext.should.containEql('Test Post');
|
||||
response.plaintext.should.containEql('Unsubscribe [%%{unsubscribe_url}%%]');
|
||||
response.plaintext.should.containEql('http://example.com');
|
||||
response.html.should.containEql('Test Post');
|
||||
response.html.should.containEql('Unsubscribe');
|
||||
response.html.should.containEql('http://example.com');
|
||||
response.replacements.length.should.eql(1);
|
||||
response.replacements.should.match([
|
||||
{
|
||||
id: 'unsubscribe_url',
|
||||
token: /%%\{unsubscribe_url\}%%/g
|
||||
}
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
126
ghost/email-service/test/email-segmenter.test.js
Normal file
126
ghost/email-service/test/email-segmenter.test.js
Normal file
@ -0,0 +1,126 @@
|
||||
const EmailSegmenter = require('../lib/email-segmenter');
|
||||
const sinon = require('sinon');
|
||||
|
||||
describe('Email segmenter', function () {
|
||||
describe('getMemberCount', function () {
|
||||
let membersRepository;
|
||||
let listStub;
|
||||
|
||||
beforeEach(function () {
|
||||
listStub = sinon.stub().resolves({
|
||||
meta: {
|
||||
pagination: {total: 12}
|
||||
}
|
||||
});
|
||||
membersRepository = {
|
||||
list: listStub
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
it('creates correct filter and count for members visibility with null segment', async function () {
|
||||
const emailSegmenter = new EmailSegmenter({
|
||||
membersRepository
|
||||
});
|
||||
|
||||
const response = await emailSegmenter.getMembersCount({
|
||||
id: 'newsletter-123',
|
||||
get: (key) => {
|
||||
if (key === 'visibility') {
|
||||
return 'members';
|
||||
}
|
||||
}
|
||||
}, 'all', null
|
||||
);
|
||||
listStub.calledOnce.should.be.true();
|
||||
listStub.calledWith({
|
||||
filter: 'newsletters.id:newsletter-123'
|
||||
}).should.be.true();
|
||||
response.should.eql(12);
|
||||
});
|
||||
|
||||
it('throws errors for incorrect recipient filter or visibility', async function () {
|
||||
const emailSegmenter = new EmailSegmenter({
|
||||
membersRepository
|
||||
});
|
||||
try {
|
||||
await emailSegmenter.getMembersCount({
|
||||
id: 'newsletter-123',
|
||||
get: (key) => {
|
||||
if (key === 'visibility') {
|
||||
return 'members';
|
||||
}
|
||||
}
|
||||
}, 'none', null
|
||||
);
|
||||
} catch (e) {
|
||||
e.message.should.eql('Cannot send email to "none" recipient filter');
|
||||
}
|
||||
|
||||
try {
|
||||
await emailSegmenter.getMembersCount({
|
||||
id: 'newsletter-123',
|
||||
get: (key) => {
|
||||
if (key === 'visibility') {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}, 'members', null
|
||||
);
|
||||
} catch (e) {
|
||||
e.message.should.eql('Unexpected visibility value "". Use one of the valid: "members", "paid".');
|
||||
}
|
||||
});
|
||||
|
||||
it('creates correct filter and count for paid visibility and custom recipient filter', async function () {
|
||||
const emailSegmenter = new EmailSegmenter({
|
||||
membersRepository
|
||||
});
|
||||
let response = await emailSegmenter.getMembersCount(
|
||||
{
|
||||
id: 'newsletter-123',
|
||||
get: (key) => {
|
||||
if (key === 'visibility') {
|
||||
return 'paid';
|
||||
}
|
||||
}
|
||||
},
|
||||
'labels:test',
|
||||
null
|
||||
);
|
||||
|
||||
listStub.calledOnce.should.be.true();
|
||||
listStub.calledWith({
|
||||
filter: 'newsletters.id:newsletter-123+(labels:test)+status:-free'
|
||||
}).should.be.true();
|
||||
response.should.eql(12);
|
||||
});
|
||||
|
||||
it('creates correct filter and count for paid visibility and custom segment', async function () {
|
||||
const emailSegmenter = new EmailSegmenter({
|
||||
membersRepository
|
||||
});
|
||||
let response = await emailSegmenter.getMembersCount(
|
||||
{
|
||||
id: 'newsletter-123',
|
||||
get: (key) => {
|
||||
if (key === 'visibility') {
|
||||
return 'members';
|
||||
}
|
||||
}
|
||||
},
|
||||
'labels:test',
|
||||
'status:free'
|
||||
);
|
||||
|
||||
listStub.calledOnce.should.be.true();
|
||||
listStub.calledWith({
|
||||
filter: 'newsletters.id:newsletter-123+(labels:test)+(status:free)'
|
||||
}).should.be.true();
|
||||
response.should.eql(12);
|
||||
});
|
||||
});
|
||||
});
|
100
ghost/email-service/test/sending-service.test.js
Normal file
100
ghost/email-service/test/sending-service.test.js
Normal file
@ -0,0 +1,100 @@
|
||||
const SendingService = require('../lib/sending-service');
|
||||
const sinon = require('sinon');
|
||||
const should = require('should');
|
||||
|
||||
describe('Sending service', function () {
|
||||
describe('send', function () {
|
||||
let emailProvider;
|
||||
let emailRenderer;
|
||||
let sendStub;
|
||||
|
||||
beforeEach(function () {
|
||||
sendStub = sinon.stub().resolves({
|
||||
id: 'provider-123'
|
||||
});
|
||||
|
||||
emailRenderer = {
|
||||
renderBody: sinon.stub().resolves({
|
||||
html: '<html><body>Hi {{name}}</body></html>',
|
||||
plaintext: 'Hi',
|
||||
replacements: [
|
||||
{
|
||||
id: 'name',
|
||||
token: '{{name}}',
|
||||
getValue: (member) => {
|
||||
return member.name;
|
||||
}
|
||||
}
|
||||
]
|
||||
}),
|
||||
getSubject: sinon.stub().returns('Hi'),
|
||||
getFromAddress: sinon.stub().returns('ghost@example.com'),
|
||||
getReplyToAddress: sinon.stub().returns('ghost+reply@example.com')
|
||||
};
|
||||
|
||||
emailProvider = {
|
||||
send: sendStub
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
it('calls mailgun client with correct data', async function () {
|
||||
const sendingService = new SendingService({
|
||||
emailRenderer,
|
||||
emailProvider
|
||||
});
|
||||
|
||||
const response = await sendingService.send({
|
||||
post: {},
|
||||
newsletter: {},
|
||||
segment: null,
|
||||
emailId: '123',
|
||||
members: [
|
||||
{
|
||||
email: 'member@example.com',
|
||||
name: 'John'
|
||||
}
|
||||
]
|
||||
}, {
|
||||
clickTrackingEnabled: true,
|
||||
openTrackingEnabled: true
|
||||
});
|
||||
should(response.id).eql('provider-123');
|
||||
should(sendStub.calledOnce).be.true();
|
||||
sendStub.calledWith(
|
||||
{
|
||||
subject: 'Hi',
|
||||
from: 'ghost@example.com',
|
||||
replyTo: 'ghost+reply@example.com',
|
||||
html: '<html><body>Hi {{name}}</body></html>',
|
||||
plaintext: 'Hi',
|
||||
emailId: '123',
|
||||
replacementDefinitions: [
|
||||
{
|
||||
id: 'name',
|
||||
token: '{{name}}',
|
||||
getValue: sinon.match.func
|
||||
}
|
||||
],
|
||||
recipients: [
|
||||
{
|
||||
email: 'member@example.com',
|
||||
replacements: [{
|
||||
id: 'name',
|
||||
token: '{{name}}',
|
||||
value: 'John'
|
||||
}]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
clickTrackingEnabled: true,
|
||||
openTrackingEnabled: true
|
||||
}
|
||||
).should.be.true();
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue
Block a user