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:
Rishabh Garg 2022-12-02 00:33:28 +05:30 committed by GitHub
parent c74857052f
commit f9161876b3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 567 additions and 6 deletions

View File

@ -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});

View File

@ -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
}
]);
});
});
});

View 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);
});
});
});

View 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();
});
});
});