Ghost/ghost/staff-service/test/staff-service.test.js
Rishabh Garg 61afa94a4e
Fixed plaintext for mentions email report (#16517)
closes https://github.com/TryGhost/Team/issues/2754

Updates plaintext version for mentions email report to include all unique mentions in a nice list format.
2023-03-29 00:36:05 +05:30

936 lines
35 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Switch these lines once there are useful utils
// const testUtils = require('./utils');
const sinon = require('sinon');
const {MemberCreatedEvent, SubscriptionCancelledEvent, SubscriptionActivatedEvent} = require('@tryghost/member-events');
const {MilestoneCreatedEvent} = require('@tryghost/milestones');
// Stuff we are testing
const DomainEvents = require('@tryghost/domain-events');
require('./utils');
const StaffService = require('../index');
function testCommonMailData({mailStub, getEmailAlertUsersStub}) {
getEmailAlertUsersStub.calledWith(
sinon.match.string,
sinon.match({transacting: {}, forUpdate: true})
).should.be.true();
// has right from/to address
mailStub.calledWith(sinon.match({
from: 'ghost@ghost.example',
to: 'owner@ghost.org'
})).should.be.true();
// Email HTML contains important bits
// Has accent color
mailStub.calledWith(
sinon.match.has('html', sinon.match('#ffffff'))
).should.be.true();
// Has email
mailStub.calledWith(
sinon.match.has('html', sinon.match('member@example.com'))
).should.be.true();
// Has member url
mailStub.calledWith(
sinon.match.has('html', sinon.match('https://admin.ghost.example/#/members/abc'))
).should.be.true();
// Has site url
mailStub.calledWith(
sinon.match.has('html', sinon.match('https://ghost.example'))
).should.be.true();
// Has staff admin url
mailStub.calledWith(
sinon.match.has('html', sinon.match('https://admin.ghost.example/#/settings/staff/ghost'))
).should.be.true();
}
function testCommonPaidSubMailData({member, mailStub, getEmailAlertUsersStub}) {
testCommonMailData({mailStub, getEmailAlertUsersStub});
getEmailAlertUsersStub.calledWith('paid-started').should.be.true();
if (member?.name) {
mailStub.calledWith(
sinon.match({subject: '💸 Paid subscription started: Ghost'})
).should.be.true();
mailStub.calledWith(
sinon.match.has('html', sinon.match('💸 Paid subscription started: Ghost'))
).should.be.true();
} else {
mailStub.calledWith(
sinon.match({subject: '💸 Paid subscription started: member@example.com'})
).should.be.true();
mailStub.calledWith(
sinon.match.has('html', sinon.match('💸 Paid subscription started: member@example.com'))
).should.be.true();
}
mailStub.calledWith(
sinon.match.has('html', sinon.match('Test Tier'))
).should.be.true();
mailStub.calledWith(
sinon.match.has('html', sinon.match('$50.00/month'))
).should.be.true();
mailStub.calledWith(
sinon.match.has('html', sinon.match('Subscription started on 1 Aug 2022'))
).should.be.true();
}
function testCommonPaidSubCancelMailData({mailStub, getEmailAlertUsersStub}) {
testCommonMailData({mailStub, getEmailAlertUsersStub});
getEmailAlertUsersStub.calledWith('paid-canceled').should.be.true();
mailStub.calledWith(
sinon.match({subject: '⚠️ Cancellation: Ghost'})
).should.be.true();
mailStub.calledWith(
sinon.match.has('html', sinon.match('⚠️ Cancellation: Ghost'))
).should.be.true();
mailStub.calledWith(
sinon.match.has('html', sinon.match('Test Tier'))
).should.be.true();
mailStub.calledWith(
sinon.match.has('html', sinon.match('$50.00/month'))
).should.be.true();
}
describe('StaffService', function () {
describe('Constructor', function () {
it('doesn\'t throw', function () {
new StaffService({});
});
});
describe('email notifications:', function () {
let mailStub;
let loggingWarningStub;
let subscribeStub;
let getEmailAlertUsersStub;
let service;
let options = {
transacting: {},
forUpdate: true
};
let stubs;
let labs = {
isSet: () => {
return false;
}
};
const settingsCache = {
get: (setting) => {
if (setting === 'title') {
return 'Ghost Site';
} else if (setting === 'accent_color') {
return '#ffffff';
}
return '';
}
};
const urlUtils = {
getSiteUrl: () => {
return 'https://ghost.example';
},
urlJoin: (adminUrl,hash,path) => {
return `${adminUrl}/${hash}${path}`;
},
urlFor: () => {
return 'https://admin.ghost.example';
}
};
const settingsHelpers = {
getDefaultEmailDomain: () => {
return 'ghost.example';
}
};
beforeEach(function () {
loggingWarningStub = sinon.stub().resolves();
mailStub = sinon.stub().resolves();
subscribeStub = sinon.stub().resolves();
getEmailAlertUsersStub = sinon.stub().resolves([{
email: 'owner@ghost.org',
slug: 'ghost'
}]);
service = new StaffService({
logging: {
warn: loggingWarningStub,
error: () => {}
},
models: {
User: {
getEmailAlertUsers: getEmailAlertUsersStub
}
},
mailer: {
send: mailStub
},
DomainEvents: {
subscribe: subscribeStub
},
settingsCache,
urlUtils,
settingsHelpers,
labs
});
stubs = {mailStub, getEmailAlertUsersStub};
});
afterEach(function () {
sinon.restore();
});
describe('subscribeEvents', function () {
it('subscribes to events', async function () {
service.subscribeEvents();
subscribeStub.callCount.should.eql(4);
subscribeStub.calledWith(SubscriptionActivatedEvent).should.be.true();
subscribeStub.calledWith(SubscriptionCancelledEvent).should.be.true();
subscribeStub.calledWith(MemberCreatedEvent).should.be.true();
subscribeStub.calledWith(MilestoneCreatedEvent).should.be.true();
});
it('listens to events', async function () {
service = new StaffService({
logging: {
warn: () => {},
error: () => {}
},
models: {
User: {
getEmailAlertUsers: getEmailAlertUsersStub
}
},
mailer: {
send: mailStub
},
DomainEvents,
settingsCache,
urlUtils,
settingsHelpers
});
service.subscribeEvents();
sinon.spy(service, 'handleEvent');
DomainEvents.dispatch(MemberCreatedEvent.create({
source: 'member',
memberId: 'member-2'
}));
await DomainEvents.allSettled();
service.handleEvent.calledWith(MemberCreatedEvent).should.be.true();
DomainEvents.dispatch(SubscriptionActivatedEvent.create({
source: 'member',
memberId: 'member-1',
subscriptionId: 'sub-1',
offerId: 'offer-1',
tierId: 'tier-1'
}));
await DomainEvents.allSettled();
service.handleEvent.calledWith(SubscriptionActivatedEvent).should.be.true();
DomainEvents.dispatch(SubscriptionCancelledEvent.create({
source: 'member',
memberId: 'member-1',
subscriptionId: 'sub-1',
tierId: 'tier-1'
}));
await DomainEvents.allSettled();
service.handleEvent.calledWith(SubscriptionCancelledEvent).should.be.true();
DomainEvents.dispatch(MilestoneCreatedEvent.create({
milestone: {
type: 'arr',
value: '100',
currency: 'usd'
}
}));
await DomainEvents.allSettled();
service.handleEvent.calledWith(MilestoneCreatedEvent).should.be.true();
});
});
describe('handleEvent', function () {
beforeEach(function () {
const models = {
User: {
getEmailAlertUsers: sinon.stub().resolves([{
email: 'owner@ghost.org',
slug: 'ghost'
}]),
findAll: sinon.stub().resolves([{
toJSON: sinon.stub().returns({
email: 'owner@ghost.org',
slug: 'ghost'
})
}])
},
Member: {
findOne: sinon.stub().resolves({
toJSON: sinon.stub().returns({
id: '1',
email: 'jamie@example.com',
name: 'Jamie',
status: 'free',
geolocation: null,
created_at: '2022-08-01T07:30:39.882Z'
})
})
},
Product: {
findOne: sinon.stub().resolves({
toJSON: sinon.stub().returns({
id: 'tier-1',
name: 'Tier 1'
})
})
},
Offer: {
findOne: sinon.stub().resolves({
toJSON: sinon.stub().returns({
discount_amount: 1000,
duration: 'forever',
discount_type: 'fixed',
name: 'Test offer',
duration_in_months: null
})
})
},
StripeCustomerSubscription: {
findOne: sinon.stub().resolves({
toJSON: sinon.stub().returns({
id: 'sub-1',
plan: {
amount: 5000,
currency: 'USD',
interval: 'month'
},
start_date: new Date('2022-08-01T07:30:39.882Z'),
current_period_end: '2024-08-01T07:30:39.882Z',
cancellation_reason: 'Changed my mind!'
})
})
}
};
service = new StaffService({
logging: {
warn: () => {},
error: () => {}
},
models: models,
mailer: {
send: mailStub
},
DomainEvents: {
subscribe: subscribeStub
},
settingsCache,
urlUtils,
settingsHelpers,
labs: {
isSet: () => {
return false;
}
}
});
});
it('handles free member created event', async function () {
await service.handleEvent(MemberCreatedEvent, {
data: {
source: 'member',
memberId: 'member-1'
}
});
mailStub.calledWith(
sinon.match({subject: '🥳 Free member signup: Jamie'})
).should.be.true();
});
it('handles paid member created event', async function () {
await service.handleEvent(SubscriptionActivatedEvent, {
data: {
source: 'member',
memberId: 'member-1',
subscriptionId: 'sub-1',
offerId: 'offer-1',
tierId: 'tier-1'
}
});
mailStub.calledWith(
sinon.match({subject: '💸 Paid subscription started: Jamie'})
).should.be.true();
});
it('handles paid member cancellation event', async function () {
await service.handleEvent(SubscriptionCancelledEvent, {
data: {
source: 'member',
memberId: 'member-1',
subscriptionId: 'sub-1',
tierId: 'tier-1'
}
});
mailStub.calledWith(
sinon.match({subject: '⚠️ Cancellation: Jamie'})
).should.be.true();
});
it('handles milestone created event', async function () {
await service.handleEvent(MilestoneCreatedEvent, {
data: {
milestone: {
type: 'arr',
value: '1000',
currency: 'usd',
emailSentAt: Date.now()
}
}
});
mailStub.calledWith(
sinon.match({subject: `Ghost Site hit $1,000 ARR`})
).should.be.true();
});
});
describe('notifyFreeMemberSignup', function () {
it('sends free member signup alert', async function () {
const member = {
name: 'Ghost',
email: 'member@example.com',
id: 'abc',
geolocation: '{"country": "France"}',
created_at: '2022-08-01T07:30:39.882Z'
};
await service.emails.notifyFreeMemberSignup({member}, options);
mailStub.calledOnce.should.be.true();
testCommonMailData(stubs);
getEmailAlertUsersStub.calledWith('free-signup').should.be.true();
mailStub.calledWith(
sinon.match({subject: '🥳 Free member signup: Ghost'})
).should.be.true();
mailStub.calledWith(
sinon.match.has('html', sinon.match('🥳 Free member signup: Ghost'))
).should.be.true();
mailStub.calledWith(
sinon.match.has('html', sinon.match('Created on 1 Aug 2022 • France'))
).should.be.true();
});
it('sends free member signup alert without member name', async function () {
const member = {
email: 'member@example.com',
id: 'abc',
geolocation: '{"country": "France"}',
created_at: '2022-08-01T07:30:39.882Z'
};
await service.emails.notifyFreeMemberSignup({member}, options);
mailStub.calledOnce.should.be.true();
testCommonMailData(stubs);
getEmailAlertUsersStub.calledWith('free-signup').should.be.true();
mailStub.calledWith(
sinon.match({subject: '🥳 Free member signup: member@example.com'})
).should.be.true();
mailStub.calledWith(
sinon.match.has('html', sinon.match('🥳 Free member signup: member@example.com'))
).should.be.true();
mailStub.calledWith(
sinon.match.has('html', sinon.match('Created on 1 Aug 2022 • France'))
).should.be.true();
});
it('sends free member signup alert with attribution', async function () {
const member = {
name: 'Ghost',
email: 'member@example.com',
id: 'abc',
geolocation: '{"country": "France"}',
created_at: '2022-08-01T07:30:39.882Z'
};
const attribution = {
referrerSource: 'Twitter',
title: 'Welcome Post',
url: 'https://example.com/welcome'
};
await service.emails.notifyFreeMemberSignup({member, attribution}, options);
mailStub.calledOnce.should.be.true();
testCommonMailData(stubs);
getEmailAlertUsersStub.calledWith('free-signup').should.be.true();
mailStub.calledWith(
sinon.match({subject: '🥳 Free member signup: Ghost'})
).should.be.true();
mailStub.calledWith(
sinon.match.has('html', sinon.match('🥳 Free member signup: Ghost'))
).should.be.true();
mailStub.calledWith(
sinon.match.has('html', sinon.match('Created on 1 Aug 2022 • France'))
).should.be.true();
mailStub.calledWith(
sinon.match.has('html', sinon.match('Source'))
).should.be.true();
mailStub.calledWith(
sinon.match.has('html', sinon.match('Twitter'))
).should.be.true();
// check attribution page
mailStub.calledWith(
sinon.match.has('html', sinon.match('Welcome Post'))
).should.be.true();
// check attribution url
mailStub.calledWith(
sinon.match.has('html', sinon.match('https://example.com/welcome'))
).should.be.true();
});
});
describe('notifyPaidSubscriptionStart', function () {
let member;
let tier;
let offer;
let subscription;
before(function () {
member = {
name: 'Ghost',
email: 'member@example.com',
id: 'abc',
geolocation: '{"country": "France"}',
created_at: '2022-08-01T07:30:39.882Z'
};
offer = {
name: 'Half price',
duration: 'once',
type: 'percent',
amount: 50
};
tier = {
name: 'Test Tier'
};
subscription = {
amount: 5000,
currency: 'USD',
interval: 'month',
startDate: '2022-08-01T07:30:39.882Z'
};
});
it('sends paid subscription start alert with attribution', async function () {
const attribution = {
referrerSource: 'Twitter',
title: 'Welcome Post',
url: 'https://example.com/welcome'
};
await service.emails.notifyPaidSubscriptionStarted({member, offer: null, tier, subscription, attribution}, options);
mailStub.calledOnce.should.be.true();
testCommonPaidSubMailData({...stubs, member});
// check attribution text
mailStub.calledWith(
sinon.match.has('html', sinon.match('Twitter'))
).should.be.true();
// check attribution text
mailStub.calledWith(
sinon.match.has('html', sinon.match('Source'))
).should.be.true();
// check attribution page
mailStub.calledWith(
sinon.match.has('html', sinon.match('Welcome Post'))
).should.be.true();
// check attribution url
mailStub.calledWith(
sinon.match.has('html', sinon.match('https://example.com/welcome'))
).should.be.true();
});
it('sends paid subscription start alert without offer', async function () {
await service.emails.notifyPaidSubscriptionStarted({member, offer: null, tier, subscription}, options);
mailStub.calledOnce.should.be.true();
testCommonPaidSubMailData({...stubs, member});
mailStub.calledWith(
sinon.match.has('html', 'Offer')
).should.be.false();
});
it('sends paid subscription start alert without member name', async function () {
let memberData = {
email: 'member@example.com',
id: 'abc',
geolocation: '{"country": "France"}',
created_at: '2022-08-01T07:30:39.882Z'
};
await service.emails.notifyPaidSubscriptionStarted({member: memberData, offer: null, tier, subscription}, options);
mailStub.calledOnce.should.be.true();
testCommonPaidSubMailData({...stubs, member: memberData});
mailStub.calledWith(
sinon.match.has('html', 'Offer')
).should.be.false();
// check preview text
mailStub.calledWith(
sinon.match.has('html', sinon.match('Test Tier: $50.00/month'))
).should.be.true();
});
it('sends paid subscription start alert with percent offer - first payment', async function () {
await service.emails.notifyPaidSubscriptionStarted({member, offer, tier, subscription}, options);
mailStub.calledOnce.should.be.true();
testCommonPaidSubMailData({...stubs, member});
mailStub.calledWith(
sinon.match.has('html', sinon.match('Half price'))
).should.be.true();
mailStub.calledWith(
sinon.match.has('html', sinon.match('50% off'))
).should.be.true();
mailStub.calledWith(
sinon.match.has('html', sinon.match('first payment'))
).should.be.true();
// check preview text
mailStub.calledWith(
sinon.match.has('html', sinon.match('Test Tier: $50.00/month - Offer: Half price - 50% off, first payment'))
).should.be.true();
});
it('sends paid subscription start alert with fixed type offer - repeating duration', async function () {
offer = {
name: 'Save ten',
duration: 'repeating',
durationInMonths: 3,
type: 'fixed',
currency: 'USD',
amount: 1000
};
await service.emails.notifyPaidSubscriptionStarted({member, offer, tier, subscription}, options);
mailStub.calledOnce.should.be.true();
testCommonPaidSubMailData({...stubs, member});
mailStub.calledWith(
sinon.match.has('html', sinon.match('Save ten'))
).should.be.true();
mailStub.calledWith(
sinon.match.has('html', sinon.match('$10.00 off'))
).should.be.true();
mailStub.calledWith(
sinon.match.has('html', sinon.match('first 3 months'))
).should.be.true();
});
it('sends paid subscription start alert with fixed type offer - forever duration', async function () {
offer = {
name: 'Save twenty',
duration: 'forever',
type: 'fixed',
currency: 'USD',
amount: 2000
};
await service.emails.notifyPaidSubscriptionStarted({member, offer, tier, subscription}, options);
mailStub.calledOnce.should.be.true();
testCommonPaidSubMailData({...stubs, member});
mailStub.calledWith(
sinon.match.has('html', sinon.match('Save twenty'))
).should.be.true();
mailStub.calledWith(
sinon.match.has('html', sinon.match('$20.00 off'))
).should.be.true();
mailStub.calledWith(
sinon.match.has('html', sinon.match('forever'))
).should.be.true();
});
it('sends paid subscription start alert with free trial offer', async function () {
offer = {
name: 'Free week',
duration: 'trial',
type: 'trial',
amount: 7
};
await service.emails.notifyPaidSubscriptionStarted({member, offer, tier, subscription}, options);
mailStub.calledOnce.should.be.true();
testCommonPaidSubMailData({...stubs, member});
mailStub.calledWith(
sinon.match.has('html', sinon.match('Free week'))
).should.be.true();
mailStub.calledWith(
sinon.match.has('html', sinon.match('7 days free'))
).should.be.true();
});
});
describe('notifyPaidSubscriptionCancel', function () {
let member;
let tier;
let subscription;
before(function () {
member = {
name: 'Ghost',
email: 'member@example.com',
id: 'abc',
geolocation: '{"country": "France"}',
created_at: '2022-08-01T07:30:39.882Z'
};
tier = {
name: 'Test Tier'
};
subscription = {
amount: 5000,
currency: 'USD',
interval: 'month',
cancelAt: '2024-08-01T07:30:39.882Z',
canceledAt: '2022-08-05T07:30:39.882Z'
};
});
it('sends paid subscription cancel alert', async function () {
await service.emails.notifyPaidSubscriptionCanceled({member, tier, subscription: {
...subscription,
cancellationReason: 'Changed my mind!'
}}, options);
mailStub.calledOnce.should.be.true();
testCommonPaidSubCancelMailData(stubs);
mailStub.calledWith(
sinon.match.has('html', sinon.match('Subscription will expire on'))
).should.be.true();
mailStub.calledWith(
sinon.match.has('html', sinon.match('Canceled on 5 Aug 2022'))
).should.be.true();
mailStub.calledWith(
sinon.match.has('html', sinon.match('1 Aug 2024'))
).should.be.true();
mailStub.calledWith(
sinon.match.has('html', 'Offer')
).should.be.false();
mailStub.calledWith(
sinon.match.has('html', sinon.match('Reason: Changed my mind!'))
).should.be.true();
mailStub.calledWith(
sinon.match.has('html', sinon.match('Cancellation reason'))
).should.be.true();
});
it('sends paid subscription cancel alert without reason', async function () {
await service.emails.notifyPaidSubscriptionCanceled({member, tier, subscription}, options);
mailStub.calledOnce.should.be.true();
testCommonPaidSubCancelMailData(stubs);
mailStub.calledWith(
sinon.match.has('html', sinon.match('Subscription will expire on'))
).should.be.true();
mailStub.calledWith(
sinon.match.has('html', sinon.match('Canceled on 5 Aug 2022'))
).should.be.true();
mailStub.calledWith(
sinon.match.has('html', sinon.match('1 Aug 2024'))
).should.be.true();
mailStub.calledWith(
sinon.match.has('html', sinon.match('Reason: '))
).should.be.false();
mailStub.calledWith(
sinon.match.has('html', sinon.match('Cancellation reason'))
).should.be.false();
// check preview text
mailStub.calledWith(
sinon.match.has('html', sinon.match('A paid member has just canceled their subscription.'))
).should.be.true();
});
});
describe('notifyMilestoneReceived', function () {
it('send Members milestone email', async function () {
const milestone = {
type: 'members',
value: 25000,
emailSentAt: Date.now()
};
await service.emails.notifyMilestoneReceived({milestone});
getEmailAlertUsersStub.calledWith('milestone-received').should.be.true();
mailStub.calledOnce.should.be.true();
mailStub.calledWith(
sinon.match.has('html', sinon.match('Ghost Site now has 25k members'))
).should.be.true();
mailStub.calledWith(
sinon.match.has('html', sinon.match('Celebrating 25,000 signups'))
).should.be.true();
// Correct image and NO height for Members milestone
mailStub.calledWith(
sinon.match.has('html', sinon.match('src="https://static.ghost.org/v5.0.0/images/milestone-email-members-25k.png" width="580" align="center"'))
).should.be.true();
mailStub.calledWith(
sinon.match.has('html', sinon.match('Congrats, <strong>25k people</strong> have chosen to support and follow your work. Thats an audience big enough to sell out Madison Square Garden. What an incredible milestone!'))
).should.be.true();
mailStub.calledWith(
sinon.match.has('html', sinon.match('View your dashboard'))
).should.be.true();
});
it('send ARR milestone email', async function () {
const milestone = {
type: 'arr',
value: 500000,
currency: 'usd',
emailSentAt: Date.now()
};
await service.emails.notifyMilestoneReceived({milestone});
getEmailAlertUsersStub.calledWith('milestone-received').should.be.true();
mailStub.calledOnce.should.be.true();
mailStub.calledWith(
sinon.match.has('html', sinon.match('Ghost Site hit $500,000 ARR'))
).should.be.true();
mailStub.calledWith(
sinon.match.has('html', sinon.match('Congrats! You reached $500k ARR'))
).should.be.true();
// Correct image and height for ARR milestone
mailStub.calledWith(
sinon.match.has('html', sinon.match('src="https://static.ghost.org/v5.0.0/images/milestone-email-usd-500k.png" width="580" height="348" align="center"'))
).should.be.true();
mailStub.calledWith(
sinon.match.has('html', sinon.match('<strong>Ghost Site</strong> is now generating <strong>$500,000</strong> in annual recurring revenue. Congratulations &mdash; this is a significant milestone.'))
).should.be.true();
mailStub.calledWith(
sinon.match.has('html', sinon.match('Login to your dashboard'))
).should.be.true();
});
it('does not send email when no date provided', async function () {
const milestone = {
type: 'members',
value: 25000
};
await service.emails.notifyMilestoneReceived({milestone});
getEmailAlertUsersStub.calledWith('milestone-received').should.be.false();
mailStub.called.should.be.false();
});
it('does not send email when a reason not to send email was provided', async function () {
const milestone = {
type: 'members',
value: 25000,
emailSentAt: Date.now(),
meta: {
reason: 'no-email'
}
};
await service.emails.notifyMilestoneReceived({milestone});
getEmailAlertUsersStub.calledWith('milestone-received').should.be.false();
mailStub.called.should.be.false();
});
it('does not send email for a milestone without correct content', async function () {
const milestone = {
type: 'members',
value: 5000, // milestone not configured
emailSentAt: Date.now()
};
await service.emails.notifyMilestoneReceived({milestone});
getEmailAlertUsersStub.calledWith('milestone-received').should.be.false();
loggingWarningStub.calledOnce.should.be.true();
mailStub.called.should.be.false();
});
});
describe('renderText for webmentions', function () {
it('renders plaintext report for mentions', async function () {
const textTemplate = await service.emails.renderText('mention-report', {
toEmail: 'jamie@example.com',
siteDomain: 'ghost.org',
staffUrl: 'https://admin.example.com/blog/ghost/#/settings/staff/jane.',
mentions: [
{
sourceSiteTitle: 'Webmentions',
sourceUrl: 'https://webmention.io/'
},
{
sourceSiteTitle: 'Ghost Demo',
sourceUrl: 'https://demo.ghost.io/'
}
]
});
textTemplate.should.match(/- Webmentions \(https:\/\/webmention.io\/\)/);
textTemplate.should.match(/Ghost Demo \(https:\/\/demo.ghost.io\/\)/);
textTemplate.should.match(/Sent to jamie@example.com from ghost.org/);
});
});
});
});