2023-05-02 23:43:47 +03:00
|
|
|
const EmailService = require('../lib/EmailService');
|
2023-06-21 11:56:59 +03:00
|
|
|
const assert = require('assert/strict');
|
2023-01-04 16:25:29 +03:00
|
|
|
const sinon = require('sinon');
|
2023-01-04 17:22:49 +03:00
|
|
|
const {createModel, createModelClass} = require('./utils');
|
2023-01-04 16:25:29 +03:00
|
|
|
|
|
|
|
describe('Email Service', function () {
|
|
|
|
let memberCount, limited, verificicationRequired, service;
|
|
|
|
let scheduleEmail;
|
|
|
|
let settings, settingsCache;
|
|
|
|
let membersRepository;
|
|
|
|
let emailRenderer;
|
|
|
|
let sendingService;
|
2023-01-26 19:32:34 +03:00
|
|
|
let scheduleRecurringJobs;
|
2023-01-04 16:25:29 +03:00
|
|
|
|
|
|
|
beforeEach(function () {
|
|
|
|
memberCount = 123;
|
|
|
|
limited = {
|
|
|
|
emails: null, // null = not limited, true = limited and error, false = limited no error
|
|
|
|
members: null
|
|
|
|
};
|
|
|
|
verificicationRequired = false;
|
|
|
|
scheduleEmail = sinon.stub().returns();
|
2023-01-26 19:32:34 +03:00
|
|
|
scheduleRecurringJobs = sinon.stub().resolves();
|
2023-01-04 16:25:29 +03:00
|
|
|
settings = {};
|
|
|
|
settingsCache = {
|
|
|
|
get(key) {
|
|
|
|
return settings[key];
|
|
|
|
}
|
|
|
|
};
|
|
|
|
membersRepository = {
|
|
|
|
get: sinon.stub().returns(undefined)
|
|
|
|
};
|
|
|
|
emailRenderer = {
|
|
|
|
getSubject: () => {
|
|
|
|
return 'Subject';
|
|
|
|
},
|
|
|
|
getFromAddress: () => {
|
|
|
|
return 'From';
|
|
|
|
},
|
|
|
|
getReplyToAddress: () => {
|
|
|
|
return 'ReplyTo';
|
|
|
|
},
|
|
|
|
renderBody: () => {
|
|
|
|
return {
|
|
|
|
html: 'HTML',
|
|
|
|
plaintext: 'Plaintext',
|
|
|
|
replacements: []
|
|
|
|
};
|
|
|
|
}
|
|
|
|
};
|
|
|
|
sendingService = {
|
|
|
|
send: sinon.stub().returns()
|
|
|
|
};
|
|
|
|
|
|
|
|
service = new EmailService({
|
|
|
|
emailSegmenter: {
|
|
|
|
getMembersCount: () => {
|
|
|
|
return Promise.resolve(memberCount);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
limitService: {
|
|
|
|
isLimited: (type) => {
|
|
|
|
return typeof limited[type] === 'boolean';
|
|
|
|
},
|
|
|
|
errorIfIsOverLimit: (type) => {
|
|
|
|
if (limited[type]) {
|
|
|
|
throw new Error('Over limit');
|
|
|
|
}
|
|
|
|
},
|
|
|
|
errorIfWouldGoOverLimit: (type) => {
|
|
|
|
if (limited[type]) {
|
|
|
|
throw new Error('Would go over limit');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
verificationTrigger: {
|
|
|
|
checkVerificationRequired: () => {
|
|
|
|
return Promise.resolve(verificicationRequired);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
models: {
|
|
|
|
Email: createModelClass()
|
|
|
|
},
|
|
|
|
batchSendingService: {
|
|
|
|
scheduleEmail
|
|
|
|
},
|
|
|
|
settingsCache,
|
|
|
|
emailRenderer,
|
|
|
|
membersRepository,
|
2023-01-26 19:32:34 +03:00
|
|
|
sendingService,
|
|
|
|
emailAnalyticsJobs: {
|
|
|
|
scheduleRecurringJobs
|
|
|
|
}
|
2023-01-04 16:25:29 +03:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
afterEach(function () {
|
|
|
|
sinon.restore();
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('checkLimits', function () {
|
|
|
|
it('Throws if over member limit', async function () {
|
|
|
|
limited.members = true;
|
|
|
|
await assert.rejects(service.checkLimits(), /Over limit/);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('Throws if over email limit', async function () {
|
|
|
|
limited.emails = true;
|
|
|
|
await assert.rejects(service.checkLimits(), /Would go over limit/);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('Throws if verification is required', async function () {
|
|
|
|
verificicationRequired = true;
|
|
|
|
await assert.rejects(service.checkLimits(), /Email sending is temporarily disabled/);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('Does not throw if limits are enabled', async function () {
|
|
|
|
// Enable limits, but don't go over limit
|
|
|
|
limited.members = false;
|
|
|
|
limited.emails = false;
|
|
|
|
await assert.doesNotReject(service.checkLimits());
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('createEmail', function () {
|
|
|
|
it('Throws if post does not have a newsletter', async function () {
|
|
|
|
const post = createModel({
|
|
|
|
newsletter: null
|
|
|
|
});
|
|
|
|
|
|
|
|
await assert.rejects(service.createEmail(post), /The post does not have a newsletter relation/);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('Throws if post does not have an active newsletter', async function () {
|
|
|
|
const post = createModel({
|
|
|
|
id: '123',
|
|
|
|
newsletter: createModel({
|
|
|
|
status: 'archived'
|
|
|
|
})
|
|
|
|
});
|
|
|
|
|
|
|
|
await assert.rejects(service.createEmail(post), /Cannot send email to archived newsletters/);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('Creates and schedules an email', async function () {
|
|
|
|
const post = createModel({
|
|
|
|
id: '123',
|
|
|
|
newsletter: createModel({
|
|
|
|
status: 'active',
|
|
|
|
feedback_enabled: true
|
|
|
|
}),
|
|
|
|
mobiledoc: 'Mobiledoc'
|
|
|
|
});
|
|
|
|
|
|
|
|
const email = await service.createEmail(post);
|
|
|
|
sinon.assert.calledOnce(scheduleEmail);
|
2023-06-21 11:56:59 +03:00
|
|
|
assert.equal(email.get('feedback_enabled'), true);
|
|
|
|
assert.equal(email.get('newsletter_id'), post.get('newsletter').id);
|
|
|
|
assert.equal(email.get('post_id'), post.id);
|
|
|
|
assert.equal(email.get('status'), 'pending');
|
|
|
|
assert.equal(email.get('source'), post.get('mobiledoc'));
|
|
|
|
assert.equal(email.get('source_type'), 'mobiledoc');
|
2023-01-26 19:32:34 +03:00
|
|
|
sinon.assert.calledOnce(scheduleRecurringJobs);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('Ignores analytics job scheduling errors', async function () {
|
|
|
|
const post = createModel({
|
|
|
|
id: '123',
|
|
|
|
newsletter: createModel({
|
|
|
|
status: 'active',
|
|
|
|
feedback_enabled: true
|
|
|
|
}),
|
|
|
|
mobiledoc: 'Mobiledoc'
|
|
|
|
});
|
|
|
|
|
|
|
|
scheduleRecurringJobs.rejects(new Error('Test error'));
|
|
|
|
await service.createEmail(post);
|
|
|
|
sinon.assert.calledOnce(scheduleRecurringJobs);
|
2023-01-04 16:25:29 +03:00
|
|
|
});
|
|
|
|
|
|
|
|
it('Creates and schedules an email with lexical', async function () {
|
|
|
|
const post = createModel({
|
|
|
|
id: '123',
|
|
|
|
newsletter: createModel({
|
|
|
|
status: 'active',
|
|
|
|
feedback_enabled: true
|
|
|
|
}),
|
|
|
|
lexical: 'Lexical'
|
|
|
|
});
|
|
|
|
|
|
|
|
const email = await service.createEmail(post);
|
|
|
|
sinon.assert.calledOnce(scheduleEmail);
|
2023-06-21 11:56:59 +03:00
|
|
|
assert.equal(email.get('feedback_enabled'), true);
|
|
|
|
assert.equal(email.get('newsletter_id'), post.get('newsletter').id);
|
|
|
|
assert.equal(email.get('post_id'), post.id);
|
|
|
|
assert.equal(email.get('status'), 'pending');
|
|
|
|
assert.equal(email.get('source'), post.get('lexical'));
|
|
|
|
assert.equal(email.get('source_type'), 'lexical');
|
2023-01-04 16:25:29 +03:00
|
|
|
});
|
|
|
|
|
|
|
|
it('Stores the error in the email model if scheduling fails', async function () {
|
|
|
|
const post = createModel({
|
|
|
|
id: '123',
|
|
|
|
newsletter: createModel({
|
|
|
|
status: 'active',
|
|
|
|
feedback_enabled: true
|
|
|
|
})
|
|
|
|
});
|
|
|
|
|
|
|
|
scheduleEmail.throws(new Error('Test error'));
|
|
|
|
|
|
|
|
const email = await service.createEmail(post);
|
|
|
|
sinon.assert.calledOnce(scheduleEmail);
|
|
|
|
|
2023-06-21 11:56:59 +03:00
|
|
|
assert.equal(email.get('error'), 'Test error');
|
|
|
|
assert.equal(email.get('status'), 'failed');
|
2023-01-04 16:25:29 +03:00
|
|
|
});
|
|
|
|
|
|
|
|
it('Stores a default error in the email model if scheduling fails', async function () {
|
|
|
|
const post = createModel({
|
|
|
|
id: '123',
|
|
|
|
newsletter: createModel({
|
|
|
|
status: 'active',
|
|
|
|
feedback_enabled: true
|
|
|
|
})
|
|
|
|
});
|
|
|
|
|
|
|
|
scheduleEmail.throws(new Error());
|
|
|
|
|
|
|
|
const email = await service.createEmail(post);
|
|
|
|
sinon.assert.calledOnce(scheduleEmail);
|
|
|
|
|
2023-06-21 11:56:59 +03:00
|
|
|
assert.equal(email.get('error'), 'Something went wrong while scheduling the email');
|
|
|
|
assert.equal(email.get('status'), 'failed');
|
2023-01-04 16:25:29 +03:00
|
|
|
});
|
|
|
|
|
|
|
|
it('Checks limits before scheduling', async function () {
|
|
|
|
const post = createModel({
|
|
|
|
id: '123',
|
|
|
|
newsletter: createModel({
|
|
|
|
status: 'active',
|
|
|
|
feedback_enabled: true
|
|
|
|
})
|
|
|
|
});
|
|
|
|
limited.emails = true;
|
|
|
|
|
|
|
|
await assert.rejects(service.createEmail(post));
|
|
|
|
sinon.assert.notCalled(scheduleEmail);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('Retry email', function () {
|
|
|
|
it('Schedules email again', async function () {
|
|
|
|
const email = createModel({
|
|
|
|
status: 'failed',
|
2023-03-09 14:32:22 +03:00
|
|
|
error: 'Test error',
|
|
|
|
post: createModel({
|
|
|
|
status: 'published'
|
|
|
|
})
|
2023-01-04 16:25:29 +03:00
|
|
|
});
|
|
|
|
|
|
|
|
await service.retryEmail(email);
|
|
|
|
sinon.assert.calledOnce(scheduleEmail);
|
|
|
|
});
|
|
|
|
|
2023-03-09 14:32:22 +03:00
|
|
|
it('Does not schedule email again if draft', async function () {
|
|
|
|
const email = createModel({
|
|
|
|
status: 'failed',
|
|
|
|
error: 'Test error',
|
|
|
|
post: createModel({
|
|
|
|
status: 'draft'
|
|
|
|
})
|
|
|
|
});
|
|
|
|
|
|
|
|
await assert.rejects(service.retryEmail(email));
|
|
|
|
sinon.assert.notCalled(scheduleEmail);
|
|
|
|
});
|
|
|
|
|
2023-01-04 16:25:29 +03:00
|
|
|
it('Checks limits before scheduling', async function () {
|
|
|
|
const email = createModel({
|
|
|
|
status: 'failed',
|
|
|
|
error: 'Test error'
|
|
|
|
});
|
|
|
|
|
|
|
|
limited.emails = true;
|
|
|
|
assert.rejects(service.retryEmail(email));
|
|
|
|
sinon.assert.notCalled(scheduleEmail);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('getExampleMember', function () {
|
|
|
|
it('Returns a member', async function () {
|
|
|
|
const member = createModel({
|
|
|
|
uuid: '123',
|
|
|
|
name: 'Example member',
|
2023-03-22 13:52:41 +03:00
|
|
|
email: 'example@example.com',
|
|
|
|
status: 'free'
|
2023-01-04 16:25:29 +03:00
|
|
|
});
|
|
|
|
membersRepository.get.resolves(member);
|
2023-03-22 13:52:41 +03:00
|
|
|
const exampleMember = await service.getExampleMember('example@example.com', 'status:free');
|
2023-06-21 11:56:59 +03:00
|
|
|
assert.equal(exampleMember.id, member.id);
|
|
|
|
assert.equal(exampleMember.name, member.get('name'));
|
|
|
|
assert.equal(exampleMember.email, member.get('email'));
|
|
|
|
assert.equal(exampleMember.uuid, member.get('uuid'));
|
|
|
|
assert.equal(exampleMember.status, 'free');
|
2023-03-22 13:52:41 +03:00
|
|
|
assert.deepEqual(exampleMember.subscriptions, []);
|
|
|
|
assert.deepEqual(exampleMember.tiers, []);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('Returns a paid member', async function () {
|
|
|
|
const member = createModel({
|
|
|
|
uuid: '123',
|
|
|
|
name: 'Example member',
|
|
|
|
email: 'example@example.com',
|
|
|
|
status: 'paid',
|
|
|
|
stripeSubscriptions: [
|
|
|
|
createModel({
|
|
|
|
status: 'active',
|
|
|
|
current_period_end: new Date(2050, 0, 1),
|
|
|
|
cancel_at_period_end: false
|
|
|
|
})
|
|
|
|
],
|
|
|
|
products: [createModel({
|
|
|
|
name: 'Silver',
|
|
|
|
expiry_at: null
|
|
|
|
})]
|
|
|
|
});
|
|
|
|
membersRepository.get.resolves(member);
|
|
|
|
const exampleMember = await service.getExampleMember('example@example.com', 'status:-free');
|
2023-06-21 11:56:59 +03:00
|
|
|
assert.equal(exampleMember.id, member.id);
|
|
|
|
assert.equal(exampleMember.name, member.get('name'));
|
|
|
|
assert.equal(exampleMember.email, member.get('email'));
|
|
|
|
assert.equal(exampleMember.uuid, member.get('uuid'));
|
|
|
|
assert.equal(exampleMember.status, 'paid');
|
2023-03-22 13:52:41 +03:00
|
|
|
assert.deepEqual(exampleMember.subscriptions, [
|
|
|
|
{
|
|
|
|
status: 'active',
|
|
|
|
current_period_end: new Date(2050, 0, 1),
|
|
|
|
cancel_at_period_end: false,
|
|
|
|
id: member.related('stripeSubscriptions')[0].id
|
|
|
|
}
|
|
|
|
]);
|
|
|
|
assert.deepEqual(exampleMember.tiers, [
|
|
|
|
{
|
|
|
|
name: 'Silver',
|
|
|
|
expiry_at: null,
|
|
|
|
id: member.related('products')[0].id
|
|
|
|
}
|
|
|
|
]);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('Returns a forced free member', async function () {
|
|
|
|
const member = createModel({
|
|
|
|
uuid: '123',
|
|
|
|
name: 'Example member',
|
|
|
|
email: 'example@example.com',
|
|
|
|
status: 'paid'
|
|
|
|
});
|
|
|
|
membersRepository.get.resolves(member);
|
|
|
|
const exampleMember = await service.getExampleMember('example@example.com', 'status:free');
|
2023-06-21 11:56:59 +03:00
|
|
|
assert.equal(exampleMember.id, member.id);
|
|
|
|
assert.equal(exampleMember.name, member.get('name'));
|
|
|
|
assert.equal(exampleMember.email, member.get('email'));
|
|
|
|
assert.equal(exampleMember.uuid, member.get('uuid'));
|
|
|
|
assert.equal(exampleMember.status, 'free');
|
2023-03-22 13:52:41 +03:00
|
|
|
assert.deepEqual(exampleMember.subscriptions, []);
|
|
|
|
assert.deepEqual(exampleMember.tiers, []);
|
2023-01-04 16:25:29 +03:00
|
|
|
});
|
|
|
|
|
|
|
|
it('Returns a member without name if member does not exist', async function () {
|
|
|
|
membersRepository.get.resolves(undefined);
|
|
|
|
const exampleMember = await service.getExampleMember('example@example.com');
|
2023-06-21 11:56:59 +03:00
|
|
|
assert.equal(exampleMember.name, '');
|
|
|
|
assert.equal(exampleMember.email, 'example@example.com');
|
2023-01-04 16:25:29 +03:00
|
|
|
assert.ok(exampleMember.id);
|
|
|
|
assert.ok(exampleMember.uuid);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('Returns a default member', async function () {
|
|
|
|
membersRepository.get.resolves(undefined);
|
|
|
|
const exampleMember = await service.getExampleMember();
|
|
|
|
assert.ok(exampleMember.id);
|
|
|
|
assert.ok(exampleMember.uuid);
|
|
|
|
assert.ok(exampleMember.name);
|
|
|
|
assert.ok(exampleMember.email);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('previewEmail', function () {
|
|
|
|
it('Replaces replacements with example member', async function () {
|
|
|
|
const post = createModel({
|
|
|
|
id: '123',
|
|
|
|
newsletter: createModel({
|
|
|
|
status: 'active',
|
|
|
|
feedback_enabled: true
|
|
|
|
})
|
|
|
|
});
|
|
|
|
sinon.stub(emailRenderer, 'renderBody').resolves({
|
|
|
|
html: 'Hello {name}, {name}',
|
|
|
|
plaintext: 'Hello {name}',
|
|
|
|
replacements: [
|
|
|
|
{
|
|
|
|
id: 'name',
|
|
|
|
token: /{name}/g,
|
|
|
|
getValue: (member) => {
|
|
|
|
return member.name;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
]
|
|
|
|
});
|
|
|
|
|
|
|
|
const data = await service.previewEmail(post, post.get('newsletter'), null);
|
2023-06-21 11:56:59 +03:00
|
|
|
assert.equal(data.html, 'Hello Jamie Larson, Jamie Larson');
|
|
|
|
assert.equal(data.plaintext, 'Hello Jamie Larson');
|
|
|
|
assert.equal(data.subject, 'Subject');
|
2023-01-04 16:25:29 +03:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('sendTestEmail', function () {
|
|
|
|
it('Sends a test email', async function () {
|
|
|
|
const post = createModel({
|
|
|
|
id: '123',
|
|
|
|
newsletter: createModel({
|
|
|
|
status: 'active',
|
|
|
|
feedback_enabled: true
|
|
|
|
})
|
|
|
|
});
|
|
|
|
await service.sendTestEmail(post, post.get('newsletter'), null, ['example@example.com']);
|
|
|
|
sinon.assert.calledOnce(sendingService.send);
|
|
|
|
const members = sendingService.send.firstCall.args[0].members;
|
2024-01-03 19:08:56 +03:00
|
|
|
const options = sendingService.send.firstCall.args[1];
|
2023-06-21 11:56:59 +03:00
|
|
|
assert.equal(members.length, 1);
|
|
|
|
assert.equal(members[0].email, 'example@example.com');
|
2024-01-03 19:08:56 +03:00
|
|
|
assert.equal(options.isTestEmail, true);
|
2023-01-04 16:25:29 +03:00
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|