const {EmailRenderer} = require('../');
const assert = require('assert/strict');
const cheerio = require('cheerio');
const {createModel, createModelClass} = require('./utils');
const linkReplacer = require('@tryghost/link-replacer');
const sinon = require('sinon');
const logging = require('@tryghost/logging');
const {HtmlValidate} = require('html-validate');
const crypto = require('crypto');
async function validateHtml(html) {
const htmlvalidate = new HtmlValidate({
extends: [
'html-validate:document',
'html-validate:standard'
],
rules: {
// We need deprecated attrs for legacy tables in older email clients
'no-deprecated-attr': 'off',
// Don't care that the first Lexical Test Mobiledoc Test Lexical Test members only section Lexical Test Mobiledoc Test Test footer
'heading-level': 'off'
},
elements: [
'html5',
// By default, html-validate requires the 'lang' attribute on the tag. We don't really want that for now.
{
html: {
attributes: {
lang: {
required: false
}
}
}
}
]
});
const report = await htmlvalidate.validateString(html);
// Improve debugging and show a snippet of the invalid HTML instead of just the line number or a huge HTML-dump
const parsedErrors = [];
if (!report.valid) {
const lines = html.split('\n');
const messages = report.results[0].messages;
for (const item of messages) {
if (item.severity !== 2) {
// Ignore warnings
continue;
}
const start = Math.max(item.line - 4, 0);
const end = Math.min(item.line + 4, lines.length - 1);
const _html = lines.slice(start, end).map(l => l.trim()).join('\n');
parsedErrors.push(`${item.ruleId}: ${item.message}\n At line ${item.line}, col ${item.column}\n HTML-snippet:\n${_html}`);
}
}
// Fail if invalid HTML
assert.equal(report.valid, true, 'Expected valid HTML without warnings, got errors:\n' + parsedErrors.join('\n\n'));
}
const getMembersValidationKey = () => {
return 'members-key';
};
describe('Email renderer', function () {
let logStub;
beforeEach(function () {
logStub = sinon.stub(logging, 'error');
});
afterEach(function () {
sinon.restore();
});
describe('buildReplacementDefinitions', function () {
let emailRenderer;
let newsletter;
let member;
let labsEnabled = false;
beforeEach(function () {
labsEnabled = false;
emailRenderer = new EmailRenderer({
urlUtils: {
urlFor: () => 'http://example.com/subdirectory/'
},
labs: {
isSet: () => labsEnabled
},
settingsCache: {
get: (key) => {
if (key === 'timezone') {
return 'UTC';
}
}
},
settingsHelpers: {getMembersValidationKey}
});
newsletter = createModel({
uuid: 'newsletteruuid'
});
member = {
id: '456',
uuid: 'myuuid',
name: 'Test User',
email: 'test@example.com',
createdAt: new Date(2023, 2, 13, 12, 0),
status: 'free'
};
});
it('returns the unsubscribe header replacement by default', function () {
const html = 'Hello world';
const replacements = emailRenderer.buildReplacementDefinitions({html, newsletterUuid: newsletter.get('uuid')});
assert.equal(replacements.length, 1);
assert.equal(replacements[0].token.toString(), '/%%\\{list_unsubscribe\\}%%/g');
assert.equal(replacements[0].id, 'list_unsubscribe');
const memberHmac = crypto.createHmac('sha256', getMembersValidationKey()).update(member.uuid).digest('hex');
assert.equal(replacements[0].getValue(member), `http://example.com/subdirectory/unsubscribe/?uuid=${member.uuid}&key=${memberHmac}&newsletter=newsletteruuid`);
});
it('returns a replacement if it is used', function () {
const html = 'Hello world %%{uuid}%%';
const replacements = emailRenderer.buildReplacementDefinitions({html, newsletterUuid: newsletter.get('uuid')});
assert.equal(replacements.length, 2);
assert.equal(replacements[0].token.toString(), '/%%\\{uuid\\}%%/g');
assert.equal(replacements[0].id, 'uuid');
assert.equal(replacements[0].getValue(member), 'myuuid');
});
it('returns a replacement only once if used multiple times', function () {
const html = 'Hello world %%{uuid}%% And %%{uuid}%%';
const replacements = emailRenderer.buildReplacementDefinitions({html, newsletterUuid: newsletter.get('uuid')});
assert.equal(replacements.length, 2);
assert.equal(replacements[0].token.toString(), '/%%\\{uuid\\}%%/g');
assert.equal(replacements[0].id, 'uuid');
assert.equal(replacements[0].getValue(member), 'myuuid');
});
it('returns correct first name', function () {
const html = 'Hello %%{first_name}%%,';
const replacements = emailRenderer.buildReplacementDefinitions({html, newsletterUuid: newsletter.get('uuid')});
assert.equal(replacements.length, 2);
assert.equal(replacements[0].token.toString(), '/%%\\{first_name\\}%%/g');
assert.equal(replacements[0].id, 'first_name');
assert.equal(replacements[0].getValue(member), 'Test');
});
it('returns correct unsubscribe url', function () {
const html = 'Hello %%{unsubscribe_url}%%,';
const replacements = emailRenderer.buildReplacementDefinitions({html, newsletterUuid: newsletter.get('uuid')});
assert.equal(replacements.length, 2);
assert.equal(replacements[0].token.toString(), '/%%\\{unsubscribe_url\\}%%/g');
assert.equal(replacements[0].id, 'unsubscribe_url');
const memberHmac = crypto.createHmac('sha256', getMembersValidationKey()).update(member.uuid).digest('hex');
assert.equal(replacements[0].getValue(member), `http://example.com/subdirectory/unsubscribe/?uuid=${member.uuid}&key=${memberHmac}&newsletter=newsletteruuid`);
});
it('returns correct name', function () {
const html = 'Hello %%{name}%%,';
const replacements = emailRenderer.buildReplacementDefinitions({html, newsletterUuid: newsletter.get('uuid')});
assert.equal(replacements.length, 2);
assert.equal(replacements[0].token.toString(), '/%%\\{name\\}%%/g');
assert.equal(replacements[0].id, 'name');
assert.equal(replacements[0].getValue(member), 'Test User');
});
it('returns hidden class for missing name', function () {
member.name = '';
const html = 'Hello %%{name_class}%%,';
const replacements = emailRenderer.buildReplacementDefinitions({html, newsletterUuid: newsletter.get('uuid')});
assert.equal(replacements.length, 2);
assert.equal(replacements[0].token.toString(), '/%%\\{name_class\\}%%/g');
assert.equal(replacements[0].id, 'name_class');
assert.equal(replacements[0].getValue(member), 'hidden');
});
it('returns empty class for available name', function () {
const html = 'Hello %%{name_class}%%,';
const replacements = emailRenderer.buildReplacementDefinitions({html, newsletterUuid: newsletter.get('uuid')});
assert.equal(replacements.length, 2);
assert.equal(replacements[0].token.toString(), '/%%\\{name_class\\}%%/g');
assert.equal(replacements[0].id, 'name_class');
assert.equal(replacements[0].getValue(member), '');
});
it('returns correct email', function () {
const html = 'Hello %%{email}%%,';
const replacements = emailRenderer.buildReplacementDefinitions({html, newsletterUuid: newsletter.get('uuid')});
assert.equal(replacements.length, 2);
assert.equal(replacements[0].token.toString(), '/%%\\{email\\}%%/g');
assert.equal(replacements[0].id, 'email');
assert.equal(replacements[0].getValue(member), 'test@example.com');
});
it('returns correct status', function () {
const html = 'Hello %%{status}%%,';
const replacements = emailRenderer.buildReplacementDefinitions({html, newsletterUuid: newsletter.get('uuid')});
assert.equal(replacements.length, 2);
assert.equal(replacements[0].token.toString(), '/%%\\{status\\}%%/g');
assert.equal(replacements[0].id, 'status');
assert.equal(replacements[0].getValue(member), 'free');
});
it('returns mapped complimentary status', function () {
member.status = 'comped';
const html = 'Hello %%{status}%%,';
const replacements = emailRenderer.buildReplacementDefinitions({html, newsletterUuid: newsletter.get('uuid')});
assert.equal(replacements.length, 2);
assert.equal(replacements[0].token.toString(), '/%%\\{status\\}%%/g');
assert.equal(replacements[0].id, 'status');
assert.equal(replacements[0].getValue(member), 'complimentary');
});
it('returns mapped trialing status', function () {
member.status = 'paid';
member.subscriptions = [
{
status: 'trialing',
trial_end_at: new Date(2050, 2, 13, 12, 0),
current_period_end: new Date(2023, 2, 13, 12, 0),
cancel_at_period_end: false
}
];
const html = 'Hello %%{status}%%,';
const replacements = emailRenderer.buildReplacementDefinitions({html, newsletterUuid: newsletter.get('uuid')});
assert.equal(replacements.length, 2);
assert.equal(replacements[0].token.toString(), '/%%\\{status\\}%%/g');
assert.equal(replacements[0].id, 'status');
assert.equal(replacements[0].getValue(member), 'trialing');
});
it('returns manage_account_url', function () {
const html = 'Hello %%{manage_account_url}%%,';
const replacements = emailRenderer.buildReplacementDefinitions({html, newsletterUuid: newsletter.get('uuid')});
assert.equal(replacements.length, 2);
assert.equal(replacements[0].token.toString(), '/%%\\{manage_account_url\\}%%/g');
assert.equal(replacements[0].id, 'manage_account_url');
assert.equal(replacements[0].getValue(member), 'http://example.com/subdirectory/#/portal/account');
});
it('returns status_text', function () {
const html = 'Hello %%{status_text}%%,';
member.status = 'paid';
member.subscriptions = [
{
status: 'trialing',
trial_end_at: new Date(2050, 2, 13, 12, 0),
current_period_end: new Date(2023, 2, 13, 12, 0),
cancel_at_period_end: false
}
];
const replacements = emailRenderer.buildReplacementDefinitions({html, newsletterUuid: newsletter.get('uuid')});
assert.equal(replacements.length, 2);
assert.equal(replacements[0].token.toString(), '/%%\\{status_text\\}%%/g');
assert.equal(replacements[0].id, 'status_text');
assert.equal(replacements[0].getValue(member), 'Your free trial ends on 13 March 2050, at which time you will be charged the regular price. You can always cancel before then.');
});
it('returns correct createdAt', function () {
const html = 'Hello %%{created_at}%%,';
const replacements = emailRenderer.buildReplacementDefinitions({html, newsletterUuid: newsletter.get('uuid')});
assert.equal(replacements.length, 2);
assert.equal(replacements[0].token.toString(), '/%%\\{created_at\\}%%/g');
assert.equal(replacements[0].id, 'created_at');
assert.equal(replacements[0].getValue(member), '13 March 2023');
});
it('returns missing created at', function () {
member.createdAt = null;
const html = 'Hello %%{created_at}%%,';
const replacements = emailRenderer.buildReplacementDefinitions({html, newsletterUuid: newsletter.get('uuid')});
assert.equal(replacements.length, 2);
assert.equal(replacements[0].token.toString(), '/%%\\{created_at\\}%%/g');
assert.equal(replacements[0].id, 'created_at');
assert.equal(replacements[0].getValue(member), '');
});
it('supports fallback values', function () {
const html = 'Hey %%{first_name, "there"}%%,';
const replacements = emailRenderer.buildReplacementDefinitions({html, newsletterUuid: newsletter.get('uuid')});
assert.equal(replacements.length, 2);
assert.equal(replacements[0].token.toString(), '/%%\\{first_name, (?:"|")there(?:"|")\\}%%/g');
assert.equal(replacements[0].id, 'first_name_2');
assert.equal(replacements[0].getValue(member), 'Test');
// In case of empty name
assert.equal(replacements[0].getValue({name: ''}), 'there');
});
it('supports combination of multiple fallback values', function () {
const html = 'Hey %%{first_name, "there"}%%, %%{first_name, "member"}%% %%{first_name}%% %%{first_name, "there"}%%';
const replacements = emailRenderer.buildReplacementDefinitions({html, newsletterUuid: newsletter.get('uuid')});
assert.equal(replacements.length, 4);
assert.equal(replacements[0].token.toString(), '/%%\\{first_name, (?:"|")there(?:"|")\\}%%/g');
assert.equal(replacements[0].id, 'first_name_2');
assert.equal(replacements[0].getValue(member), 'Test');
// In case of empty name
assert.equal(replacements[0].getValue({name: ''}), 'there');
assert.equal(replacements[1].token.toString(), '/%%\\{first_name, (?:"|")member(?:"|")\\}%%/g');
assert.equal(replacements[1].id, 'first_name_3');
assert.equal(replacements[1].getValue(member), 'Test');
// In case of empty name
assert.equal(replacements[1].getValue({name: ''}), 'member');
assert.equal(replacements[2].token.toString(), '/%%\\{first_name\\}%%/g');
assert.equal(replacements[2].id, 'first_name');
assert.equal(replacements[2].getValue(member), 'Test');
// In case of empty name
assert.equal(replacements[2].getValue({name: ''}), '');
});
it('handles members uuid and key', function () {
const html = '%%{uuid}%% %%{key}%%';
const replacements = emailRenderer.buildReplacementDefinitions({html, newsletterUuid: newsletter.get('uuid')});
assert.equal(replacements.length, 3);
assert.equal(replacements[0].token.toString(), '/%%\\{uuid\\}%%/g');
assert.equal(replacements[0].id, 'uuid');
assert.equal(replacements[0].getValue(member), 'myuuid');
assert.equal(replacements[1].token.toString(), '/%%\\{key\\}%%/g');
assert.equal(replacements[1].id, 'key');
const memberHmac = crypto.createHmac('sha256', getMembersValidationKey()).update(member.uuid).digest('hex');
assert.equal(replacements[1].getValue(member), memberHmac);
});
});
describe('isMemberTrialing', function () {
let emailRenderer;
beforeEach(function () {
emailRenderer = new EmailRenderer({
urlUtils: {
urlFor: () => 'http://example.com/subdirectory/'
},
labs: {
isSet: () => false
},
settingsCache: {
get: (key) => {
if (key === 'timezone') {
return 'UTC';
}
}
}
});
});
it('Returns false for free member', function () {
const member = {
id: '456',
uuid: 'myuuid',
name: 'Test User',
email: 'test@example.com',
createdAt: new Date(2023, 2, 13, 12, 0),
status: 'free'
};
const result = emailRenderer.isMemberTrialing(member);
assert.equal(result, false);
});
it('Returns false for paid member without trial', function () {
const member = {
id: '456',
uuid: 'myuuid',
name: 'Test User',
email: 'test@example.com',
createdAt: new Date(2023, 2, 13, 12, 0),
status: 'paid',
subscriptions: [
{
status: 'active',
current_period_end: new Date(2023, 2, 13, 12, 0),
cancel_at_period_end: false
}
]
};
const result = emailRenderer.isMemberTrialing(member);
assert.equal(result, false);
});
it('Returns true for trialing paid member', function () {
const member = {
id: '456',
uuid: 'myuuid',
name: 'Test User',
email: 'test@example.com',
createdAt: new Date(2023, 2, 13, 12, 0),
status: 'paid',
subscriptions: [
{
status: 'trialing',
trial_end_at: new Date(2050, 2, 13, 12, 0),
current_period_end: new Date(2023, 2, 13, 12, 0),
cancel_at_period_end: false
}
],
tiers: []
};
const result = emailRenderer.isMemberTrialing(member);
assert.equal(result, true);
});
it('Returns false for expired trialing paid member', function () {
const member = {
id: '456',
uuid: 'myuuid',
name: 'Test User',
email: 'test@example.com',
createdAt: new Date(2023, 2, 13, 12, 0),
status: 'paid',
subscriptions: [
{
status: 'trialing',
trial_end_at: new Date(2000, 2, 13, 12, 0),
current_period_end: new Date(2023, 2, 13, 12, 0),
cancel_at_period_end: false
}
],
tiers: []
};
const result = emailRenderer.isMemberTrialing(member);
assert.equal(result, false);
});
});
describe('getMemberStatusText', function () {
let emailRenderer;
beforeEach(function () {
emailRenderer = new EmailRenderer({
urlUtils: {
urlFor: () => 'http://example.com/subdirectory/'
},
labs: {
isSet: () => false
},
settingsCache: {
get: (key) => {
if (key === 'timezone') {
return 'UTC';
}
}
}
});
});
it('Returns for free member', function () {
const member = {
id: '456',
uuid: 'myuuid',
name: 'Test User',
email: 'test@example.com',
createdAt: new Date(2023, 2, 13, 12, 0),
status: 'free'
};
const result = emailRenderer.getMemberStatusText(member);
assert.equal(result, '');
});
it('Returns for active paid member', function () {
const member = {
id: '456',
uuid: 'myuuid',
name: 'Test User',
email: 'test@example.com',
createdAt: new Date(2023, 2, 13, 12, 0),
status: 'paid',
subscriptions: [
{
status: 'active',
current_period_end: new Date(2023, 2, 13, 12, 0),
cancel_at_period_end: false
}
]
};
const result = emailRenderer.getMemberStatusText(member);
assert.equal(result, 'Your subscription will renew on 13 March 2023.');
});
it('Returns for canceled paid member', function () {
const member = {
id: '456',
uuid: 'myuuid',
name: 'Test User',
email: 'test@example.com',
createdAt: new Date(2023, 2, 13, 12, 0),
status: 'paid',
subscriptions: [
{
status: 'active',
current_period_end: new Date(2023, 2, 13, 12, 0),
cancel_at_period_end: true
}
]
};
const result = emailRenderer.getMemberStatusText(member);
assert.equal(result, 'Your subscription has been canceled and will expire on 13 March 2023. You can resume your subscription via your account settings.');
});
it('Returns for expired paid member', function () {
const member = {
id: '456',
uuid: 'myuuid',
name: 'Test User',
email: 'test@example.com',
createdAt: new Date(2023, 2, 13, 12, 0),
status: 'paid',
subscriptions: [
{
status: 'canceled',
current_period_end: new Date(2023, 2, 13, 12, 0),
cancel_at_period_end: true
}
],
tiers: []
};
const result = emailRenderer.getMemberStatusText(member);
assert.equal(result, 'Your subscription has expired.');
});
it('Returns for trialing paid member', function () {
const member = {
id: '456',
uuid: 'myuuid',
name: 'Test User',
email: 'test@example.com',
createdAt: new Date(2023, 2, 13, 12, 0),
status: 'paid',
subscriptions: [
{
status: 'trialing',
trial_end_at: new Date(2050, 2, 13, 12, 0),
current_period_end: new Date(2023, 2, 13, 12, 0),
cancel_at_period_end: false
}
],
tiers: []
};
const result = emailRenderer.getMemberStatusText(member);
assert.equal(result, 'Your free trial ends on 13 March 2050, at which time you will be charged the regular price. You can always cancel before then.');
});
it('Returns for infinite complimentary member', function () {
const member = {
id: '456',
uuid: 'myuuid',
name: 'Test User',
email: 'test@example.com',
createdAt: new Date(2023, 2, 13, 12, 0),
status: 'comped',
subscriptions: [],
tiers: [
{
name: 'Silver',
expiry_at: null
}
]
};
const result = emailRenderer.getMemberStatusText(member);
assert.equal(result, '');
});
it('Returns for expiring complimentary member', function () {
const member = {
id: '456',
uuid: 'myuuid',
name: 'Test User',
email: 'test@example.com',
createdAt: new Date(2023, 2, 13, 12, 0),
status: 'comped',
subscriptions: [],
tiers: [
{
name: 'Silver',
expiry_at: new Date(2050, 2, 13, 12, 0)
}
]
};
const result = emailRenderer.getMemberStatusText(member);
assert.equal(result, 'Your subscription will expire on 13 March 2050.');
});
it('Returns for a paid member without subscriptions', function () {
const member = {
id: '456',
uuid: 'myuuid',
name: 'Test User',
email: 'test@example.com',
createdAt: new Date(2023, 2, 13, 12, 0),
status: 'paid',
subscriptions: [],
tiers: [
{
name: 'Silver',
expiry_at: new Date(2050, 2, 13, 12, 0)
}
]
};
const result = emailRenderer.getMemberStatusText(member);
assert.equal(result, 'Your subscription has been canceled and will expire on 13 March 2050. You can resume your subscription via your account settings.');
});
it('Returns for an infinte paid member without subscriptions', function () {
const member = {
id: '456',
uuid: 'myuuid',
name: 'Test User',
email: 'test@example.com',
createdAt: new Date(2023, 2, 13, 12, 0),
status: 'paid',
subscriptions: [],
tiers: [
{
name: 'Silver',
expiry_at: null
}
]
};
const result = emailRenderer.getMemberStatusText(member);
assert.equal(result, '');
});
});
describe('getSubject', function () {
const emailRenderer = new EmailRenderer({
urlUtils: {
urlFor: () => 'http://example.com'
},
labs: {
isSet: () => false
}
});
it('returns a post with correct subject from meta', function () {
const post = createModel({
posts_meta: createModel({
email_subject: 'Test Newsletter'
}),
title: 'Sample Post',
loaded: ['posts_meta']
});
let response = emailRenderer.getSubject(post);
response.should.equal('Test Newsletter');
});
it('returns a post with correct subject from title', function () {
const post = createModel({
posts_meta: createModel({
email_subject: ''
}),
title: 'Sample Post',
loaded: ['posts_meta']
});
let response = emailRenderer.getSubject(post);
response.should.equal('Sample Post');
});
it('adds [TEST] prefix for test emails', function () {
const post = createModel({
posts_meta: createModel({
email_subject: ''
}),
title: 'Sample Post',
loaded: ['posts_meta']
});
let response = emailRenderer.getSubject(post, true);
response.should.equal('[TEST] Sample Post');
});
});
describe('getFromAddress', function () {
let siteTitle = 'Test Blog';
let emailRenderer = new EmailRenderer({
settingsCache: {
get: (key) => {
if (key === 'title') {
return siteTitle;
}
}
},
settingsHelpers: {
getNoReplyAddress: () => {
return 'reply@example.com';
}
},
labs: {
isSet: () => false
},
emailAddressService: {
getAddress(addresses) {
return addresses;
}
}
});
it('returns correct from address for newsletter', function () {
const newsletter = createModel({
sender_email: 'ghost@example.com',
sender_name: 'Ghost'
});
const response = emailRenderer.getFromAddress({}, newsletter);
response.should.equal('"Ghost"
Lexical Test
'; let response = await emailRenderer.renderBody( post, newsletter, segment, options ); // Check all links have domain tracked-link.com const $ = cheerio.load(response.html); const links = []; for (const link of $('a').toArray()) { const href = $(link).attr('href'); links.push(href); if (href === '#') { continue; } if (href.includes('unsubscribe_url')) { href.should.eql('%%{unsubscribe_url}%%'); } else if (href.includes('feedback-link.com')) { href.should.containEql('%%{uuid}%%'); } else if (href.includes('https://ghost.org/?via=pbg-newsletter')) { href.should.not.containEql('tracked-link.com'); } else { href.should.containEql('tracked-link.com'); href.should.containEql('m=%%{uuid}%%'); } } // Update the following array when you make changes to the email template, check if replacements are correct for each newly added link. assert.deepEqual(links, [ `http://tracked-link.com/?m=%%{uuid}%%&url=http%3A%2F%2Fexample.com%2F%3Fsource_tracking%3DTest%2BNewsletter%26post_tracking%3Dadded`, `http://tracked-link.com/?m=%%{uuid}%%&url=http%3A%2F%2Fexample.com%2F%3Fsource_tracking%3DTest%2BNewsletter%26post_tracking%3Dadded`, `http://tracked-link.com/?m=%%{uuid}%%&url=http%3A%2F%2Fexample.com%2F%3Fsource_tracking%3DTest%2BNewsletter%26post_tracking%3Dadded`, `http://tracked-link.com/?m=%%{uuid}%%&url=https%3A%2F%2Fexternal-domain.com%2F%3Fref%3D123%26source_tracking%3Dsite`, `http://tracked-link.com/?m=%%{uuid}%%&url=https%3A%2F%2Fencoded-link.com%2F%3Fcode%3Dtest%26source_tracking%3Dsite`, `http://tracked-link.com/?m=%%{uuid}%%&url=https%3A%2F%2Fexample.com%2F%3Fref%3D123%26source_tracking%3DTest%2BNewsletter%26post_tracking%3Dadded`, '#', `http://feedback-link.com/?score=1&uuid=%%{uuid}%%&key=%%{key}%%`, `http://feedback-link.com/?score=0&uuid=%%{uuid}%%&key=%%{key}%%`, `http://feedback-link.com/?score=1&uuid=%%{uuid}%%&key=%%{key}%%`, `http://feedback-link.com/?score=0&uuid=%%{uuid}%%&key=%%{key}%%`, `%%{unsubscribe_url}%%`, `https://ghost.org/?via=pbg-newsletter&source_tracking=site` ]); // Check uuid in replacements response.replacements.length.should.eql(4); response.replacements[0].id.should.eql('uuid'); response.replacements[0].token.should.eql(/%%\{uuid\}%%/g); response.replacements[1].id.should.eql('key'); response.replacements[1].token.should.eql(/%%\{key\}%%/g); response.replacements[2].id.should.eql('unsubscribe_url'); response.replacements[2].token.should.eql(/%%\{unsubscribe_url\}%%/g); response.replacements[3].id.should.eql('list_unsubscribe'); }); it('replaces all relative links if click tracking is disabled', async function () { const post = createModel(basePost); const newsletter = createModel({ header_image: null, name: 'Test Newsletter', show_badge: true, feedback_enabled: true, show_post_title_section: true }); const segment = null; const options = { clickTrackingEnabled: false }; renderedPost = 'Lexical Test
'; let response = await emailRenderer.renderBody( post, newsletter, segment, options ); // Check all links have domain tracked-link.com const $ = cheerio.load(response.html); const links = []; for (const link of $('a').toArray()) { const href = $(link).attr('href'); links.push(href); } // Update the following array when you make changes to the email template, check if replacements are correct for each newly added link. assert.deepEqual(links, [ 'http://example.com/', 'http://example.com/', 'http://example.com/', 'http://example.com/#relative-test', '#', 'http://feedback-link.com/?score=1&uuid=%%{uuid}%%&key=%%{key}%%', 'http://feedback-link.com/?score=0&uuid=%%{uuid}%%&key=%%{key}%%', 'http://feedback-link.com/?score=1&uuid=%%{uuid}%%&key=%%{key}%%', 'http://feedback-link.com/?score=0&uuid=%%{uuid}%%&key=%%{key}%%', '%%{unsubscribe_url}%%', 'https://ghost.org/?via=pbg-newsletter' ]); }); it('handles encoded links', async function () { const post = createModel(basePost); const newsletter = createModel({ header_image: null, name: 'Test Newsletter', show_badge: true, feedback_enabled: true, show_post_title_section: true }); const segment = null; const options = { clickTrackingEnabled: true }; renderedPost = 'Lexical Test
'; let response = await emailRenderer.renderBody( post, newsletter, segment, options ); // Check all links have domain tracked-link.com const $ = cheerio.load(response.html); const links = []; for (const link of $('a').toArray()) { const href = $(link).attr('href'); links.push(href); if (href.includes('unsubscribe_url')) { href.should.eql('%%{unsubscribe_url}%%'); } else if (href.includes('feedback-link.com')) { href.should.containEql('%%{uuid}%%'); } else if (href.includes('https://ghost.org/?via=pbg-newsletter')) { href.should.not.containEql('tracked-link.com'); } else { href.should.containEql('tracked-link.com'); href.should.containEql('m=%%{uuid}%%'); } } // Update the following array when you make changes to the email template, check if replacements are correct for each newly added link. assert.deepEqual(links, [ `http://tracked-link.com/?m=%%{uuid}%%&url=http%3A%2F%2Fexample.com%2F%3Fsource_tracking%3DTest%2BNewsletter%26post_tracking%3Dadded`, `http://tracked-link.com/?m=%%{uuid}%%&url=http%3A%2F%2Fexample.com%2F%3Fsource_tracking%3DTest%2BNewsletter%26post_tracking%3Dadded`, `http://tracked-link.com/?m=%%{uuid}%%&url=http%3A%2F%2Fexample.com%2F%3Fsource_tracking%3DTest%2BNewsletter%26post_tracking%3Dadded`, `http://tracked-link.com/?m=%%{uuid}%%&url=https%3A%2F%2Fexternal-domain.com%2F%3Fref%3D123%26source_tracking%3Dsite`, `http://tracked-link.com/?m=%%{uuid}%%&url=https%3A%2F%2Fexample.com%2F%3Fref%3D123%26source_tracking%3DTest%2BNewsletter%26post_tracking%3Dadded`, `http://feedback-link.com/?score=1&uuid=%%{uuid}%%&key=%%{key}%%`, `http://feedback-link.com/?score=0&uuid=%%{uuid}%%&key=%%{key}%%`, `http://feedback-link.com/?score=1&uuid=%%{uuid}%%&key=%%{key}%%`, `http://feedback-link.com/?score=0&uuid=%%{uuid}%%&key=%%{key}%%`, `%%{unsubscribe_url}%%`, `https://ghost.org/?via=pbg-newsletter&source_tracking=site` ]); // Check uuid in replacements response.replacements.length.should.eql(4); response.replacements[0].id.should.eql('uuid'); response.replacements[0].token.should.eql(/%%\{uuid\}%%/g); response.replacements[1].id.should.eql('key'); response.replacements[1].token.should.eql(/%%\{key\}%%/g); response.replacements[2].id.should.eql('unsubscribe_url'); response.replacements[2].token.should.eql(/%%\{unsubscribe_url\}%%/g); response.replacements[3].id.should.eql('list_unsubscribe'); }); it('removes data-gh-segment and renders paywall', async function () { renderedPost = '