2022-08-25 10:25:36 +03:00
// Switch these lines once there are useful utils
// const testUtils = require('./utils');
const sinon = require ( 'sinon' ) ;
2023-02-13 15:07:53 +03:00
const { MemberCreatedEvent , SubscriptionCancelledEvent , SubscriptionActivatedEvent } = require ( '@tryghost/member-events' ) ;
2023-02-23 12:20:13 +03:00
const { MilestoneCreatedEvent } = require ( '@tryghost/milestones' ) ;
2022-08-25 10:25:36 +03:00
2023-03-12 19:11:45 +03:00
// Stuff we are testing
const DomainEvents = require ( '@tryghost/domain-events' ) ;
2022-08-25 10:25:36 +03:00
require ( './utils' ) ;
2023-02-23 12:20:13 +03:00
const StaffService = require ( '../index' ) ;
2022-08-25 10:25:36 +03:00
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 ( ) ;
2022-08-25 18:57:50 +03:00
// Has email
mailStub . calledWith (
sinon . match . has ( 'html' , sinon . match ( 'member@example.com' ) )
) . should . be . true ( ) ;
2022-08-25 10:25:36 +03:00
// 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 ( ) ;
}
2022-09-02 13:56:01 +03:00
function testCommonPaidSubMailData ( { member , mailStub , getEmailAlertUsersStub } ) {
2022-08-25 10:25:36 +03:00
testCommonMailData ( { mailStub , getEmailAlertUsersStub } ) ;
getEmailAlertUsersStub . calledWith ( 'paid-started' ) . should . be . true ( ) ;
2022-09-02 13:56:01 +03:00
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 ( ) ;
}
2022-08-25 10:25:36 +03:00
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 (
2022-08-25 22:49:55 +03:00
sinon . match ( { subject : '⚠️ Cancellation: Ghost' } )
2022-08-25 10:25:36 +03:00
) . should . be . true ( ) ;
mailStub . calledWith (
2022-08-25 22:49:55 +03:00
sinon . match . has ( 'html' , sinon . match ( '⚠️ Cancellation: Ghost' ) )
2022-08-25 10:25:36 +03:00
) . 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 ;
2023-03-21 16:39:40 +03:00
let loggingWarningStub ;
2022-09-09 17:23:43 +03:00
let subscribeStub ;
2022-08-25 10:25:36 +03:00
let getEmailAlertUsersStub ;
let service ;
let options = {
transacting : { } ,
forUpdate : true
} ;
let stubs ;
2023-03-21 16:39:40 +03:00
let labs = {
2023-03-21 18:29:04 +03:00
isSet : ( ) => {
2023-03-21 16:39:40 +03:00
return false ;
}
} ;
2022-09-02 17:57:59 +03:00
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' ;
2023-11-23 12:25:30 +03:00
} ,
useNewEmailAddresses : ( ) => {
return false ;
2022-09-02 17:57:59 +03:00
}
} ;
2022-08-25 10:25:36 +03:00
beforeEach ( function ( ) {
2023-03-21 16:39:40 +03:00
loggingWarningStub = sinon . stub ( ) . resolves ( ) ;
2022-08-25 10:25:36 +03:00
mailStub = sinon . stub ( ) . resolves ( ) ;
2022-09-09 17:23:43 +03:00
subscribeStub = sinon . stub ( ) . resolves ( ) ;
2022-08-25 10:25:36 +03:00
getEmailAlertUsersStub = sinon . stub ( ) . resolves ( [ {
email : 'owner@ghost.org' ,
slug : 'ghost'
} ] ) ;
service = new StaffService ( {
logging : {
2023-03-21 16:39:40 +03:00
warn : loggingWarningStub ,
2022-08-25 22:49:55 +03:00
error : ( ) => { }
2022-08-25 10:25:36 +03:00
} ,
models : {
User : {
getEmailAlertUsers : getEmailAlertUsersStub
}
} ,
mailer : {
send : mailStub
} ,
2022-09-09 17:23:43 +03:00
DomainEvents : {
subscribe : subscribeStub
} ,
2022-09-02 17:57:59 +03:00
settingsCache ,
urlUtils ,
2023-03-21 16:39:40 +03:00
settingsHelpers ,
labs
2022-08-25 10:25:36 +03:00
} ) ;
stubs = { mailStub , getEmailAlertUsersStub } ;
} ) ;
afterEach ( function ( ) {
sinon . restore ( ) ;
} ) ;
2022-09-09 17:23:43 +03:00
describe ( 'subscribeEvents' , function ( ) {
it ( 'subscribes to events' , async function ( ) {
service . subscribeEvents ( ) ;
2023-03-13 14:33:57 +03:00
subscribeStub . callCount . should . eql ( 4 ) ;
2023-02-13 15:07:53 +03:00
subscribeStub . calledWith ( SubscriptionActivatedEvent ) . should . be . true ( ) ;
2022-09-09 17:23:43 +03:00
subscribeStub . calledWith ( SubscriptionCancelledEvent ) . should . be . true ( ) ;
subscribeStub . calledWith ( MemberCreatedEvent ) . should . be . true ( ) ;
2023-02-23 12:20:13 +03:00
subscribeStub . calledWith ( MilestoneCreatedEvent ) . should . be . true ( ) ;
2022-09-09 17:23:43 +03:00
} ) ;
2023-03-12 19:11:45 +03:00
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 ( ) ;
} ) ;
2022-09-09 17:23:43 +03:00
} ) ;
describe ( 'handleEvent' , function ( ) {
beforeEach ( function ( ) {
const models = {
User : {
getEmailAlertUsers : sinon . stub ( ) . resolves ( [ {
email : 'owner@ghost.org' ,
slug : 'ghost'
2023-01-25 16:10:29 +03:00
} ] ) ,
findAll : sinon . stub ( ) . resolves ( [ {
toJSON : sinon . stub ( ) . returns ( {
email : 'owner@ghost.org' ,
slug : 'ghost'
} )
2022-09-09 17:23:43 +03:00
} ] )
} ,
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 ,
2023-01-25 16:10:29 +03:00
settingsHelpers ,
labs : {
2023-03-21 18:29:04 +03:00
isSet : ( ) => {
2023-02-23 12:20:13 +03:00
return false ;
}
2023-01-25 16:10:29 +03:00
}
2022-09-09 17:23:43 +03:00
} ) ;
} ) ;
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 ( ) {
2023-02-13 15:07:53 +03:00
await service . handleEvent ( SubscriptionActivatedEvent , {
2022-09-09 17:23:43 +03:00
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 ( ) ;
} ) ;
2023-02-13 15:07:53 +03:00
2023-02-23 12:20:13 +03:00
it ( 'handles milestone created event' , async function ( ) {
await service . handleEvent ( MilestoneCreatedEvent , {
data : {
milestone : {
type : 'arr' ,
2023-03-21 16:39:40 +03:00
value : '1000' ,
currency : 'usd' ,
emailSentAt : Date . now ( )
2023-02-23 12:20:13 +03:00
}
}
} ) ;
2023-03-21 16:39:40 +03:00
mailStub . calledWith (
sinon . match ( { subject : ` Ghost Site hit $ 1,000 ARR ` } )
) . should . be . true ( ) ;
2023-02-23 12:20:13 +03:00
} ) ;
2022-09-09 17:23:43 +03:00
} ) ;
2022-08-25 10:25:36 +03:00
describe ( 'notifyFreeMemberSignup' , function ( ) {
it ( 'sends free member signup alert' , async function ( ) {
const member = {
name : 'Ghost' ,
2022-08-25 18:57:50 +03:00
email : 'member@example.com' ,
2022-08-25 10:25:36 +03:00
id : 'abc' ,
2022-08-25 21:46:54 +03:00
geolocation : '{"country": "France"}' ,
2022-08-25 10:25:36 +03:00
created _at : '2022-08-01T07:30:39.882Z'
} ;
2023-03-06 12:36:47 +03:00
await service . emails . notifyFreeMemberSignup ( { member } , options ) ;
2022-08-25 10:25:36 +03:00
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 ( ) ;
} ) ;
2022-09-02 13:56:01 +03:00
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'
} ;
2023-03-06 12:36:47 +03:00
await service . emails . notifyFreeMemberSignup ( { member } , options ) ;
2022-09-02 13:56:01 +03:00
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 ( ) ;
} ) ;
2023-03-06 12:36:47 +03:00
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 = {
2023-03-10 17:44:53 +03:00
referrerSource : 'Twitter' ,
title : 'Welcome Post' ,
url : 'https://example.com/welcome'
2023-03-06 12:36:47 +03:00
} ;
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 ( ) ;
2023-03-10 17:44:53 +03:00
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
2023-03-06 12:36:47 +03:00
mailStub . calledWith (
2023-03-10 17:44:53 +03:00
sinon . match . has ( 'html' , sinon . match ( 'https://example.com/welcome' ) )
2023-03-06 12:36:47 +03:00
) . should . be . true ( ) ;
} ) ;
2022-08-25 10:25:36 +03:00
} ) ;
describe ( 'notifyPaidSubscriptionStart' , function ( ) {
let member ;
let tier ;
let offer ;
let subscription ;
before ( function ( ) {
member = {
name : 'Ghost' ,
2022-08-25 18:57:50 +03:00
email : 'member@example.com' ,
2022-08-25 10:25:36 +03:00
id : 'abc' ,
2022-08-25 21:46:54 +03:00
geolocation : '{"country": "France"}' ,
2022-08-25 10:25:36 +03:00
created _at : '2022-08-01T07:30:39.882Z'
} ;
offer = {
name : 'Half price' ,
duration : 'once' ,
type : 'percent' ,
amount : 50
} ;
tier = {
name : 'Test Tier'
} ;
subscription = {
2022-09-09 17:23:43 +03:00
amount : 5000 ,
currency : 'USD' ,
interval : 'month' ,
startDate : '2022-08-01T07:30:39.882Z'
2022-08-25 10:25:36 +03:00
} ;
} ) ;
2023-03-06 12:36:47 +03:00
it ( 'sends paid subscription start alert with attribution' , async function ( ) {
const attribution = {
2023-03-10 17:44:53 +03:00
referrerSource : 'Twitter' ,
title : 'Welcome Post' ,
url : 'https://example.com/welcome'
2023-03-06 12:36:47 +03:00
} ;
await service . emails . notifyPaidSubscriptionStarted ( { member , offer : null , tier , subscription , attribution } , options ) ;
mailStub . calledOnce . should . be . true ( ) ;
testCommonPaidSubMailData ( { ... stubs , member } ) ;
// check attribution text
mailStub . calledWith (
2023-03-10 17:44:53 +03:00
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' ) )
2023-03-06 12:36:47 +03:00
) . should . be . true ( ) ;
} ) ;
2022-08-25 10:25:36 +03:00
it ( 'sends paid subscription start alert without offer' , async function ( ) {
2022-09-09 17:23:43 +03:00
await service . emails . notifyPaidSubscriptionStarted ( { member , offer : null , tier , subscription } , options ) ;
2022-08-25 10:25:36 +03:00
mailStub . calledOnce . should . be . true ( ) ;
2022-09-02 13:56:01 +03:00
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'
} ;
2022-09-09 17:23:43 +03:00
await service . emails . notifyPaidSubscriptionStarted ( { member : memberData , offer : null , tier , subscription } , options ) ;
2022-09-02 13:56:01 +03:00
mailStub . calledOnce . should . be . true ( ) ;
testCommonPaidSubMailData ( { ... stubs , member : memberData } ) ;
2022-08-25 10:25:36 +03:00
mailStub . calledWith (
sinon . match . has ( 'html' , 'Offer' )
) . should . be . false ( ) ;
2022-10-07 13:31:05 +03:00
// check preview text
mailStub . calledWith (
sinon . match . has ( 'html' , sinon . match ( 'Test Tier: $50.00/month' ) )
) . should . be . true ( ) ;
2022-08-25 10:25:36 +03:00
} ) ;
it ( 'sends paid subscription start alert with percent offer - first payment' , async function ( ) {
2022-09-09 17:23:43 +03:00
await service . emails . notifyPaidSubscriptionStarted ( { member , offer , tier , subscription } , options ) ;
2022-08-25 10:25:36 +03:00
mailStub . calledOnce . should . be . true ( ) ;
2022-09-02 13:56:01 +03:00
testCommonPaidSubMailData ( { ... stubs , member } ) ;
2022-08-25 10:25:36 +03:00
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 ( ) ;
2022-10-07 13:31:05 +03:00
// 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 ( ) ;
2022-08-25 10:25:36 +03:00
} ) ;
it ( 'sends paid subscription start alert with fixed type offer - repeating duration' , async function ( ) {
offer = {
name : 'Save ten' ,
duration : 'repeating' ,
2022-09-09 17:23:43 +03:00
durationInMonths : 3 ,
2022-08-25 10:25:36 +03:00
type : 'fixed' ,
currency : 'USD' ,
2022-09-05 20:43:40 +03:00
amount : 1000
2022-08-25 10:25:36 +03:00
} ;
2022-09-09 17:23:43 +03:00
await service . emails . notifyPaidSubscriptionStarted ( { member , offer , tier , subscription } , options ) ;
2022-08-25 10:25:36 +03:00
mailStub . calledOnce . should . be . true ( ) ;
2022-09-02 13:56:01 +03:00
testCommonPaidSubMailData ( { ... stubs , member } ) ;
2022-08-25 10:25:36 +03:00
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' ,
2022-09-05 20:43:40 +03:00
amount : 2000
2022-08-25 10:25:36 +03:00
} ;
2022-09-09 17:23:43 +03:00
await service . emails . notifyPaidSubscriptionStarted ( { member , offer , tier , subscription } , options ) ;
2022-08-25 10:25:36 +03:00
mailStub . calledOnce . should . be . true ( ) ;
2022-09-02 13:56:01 +03:00
testCommonPaidSubMailData ( { ... stubs , member } ) ;
2022-08-25 10:25:36 +03:00
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
} ;
2022-09-09 17:23:43 +03:00
await service . emails . notifyPaidSubscriptionStarted ( { member , offer , tier , subscription } , options ) ;
2022-08-25 10:25:36 +03:00
mailStub . calledOnce . should . be . true ( ) ;
2022-09-02 13:56:01 +03:00
testCommonPaidSubMailData ( { ... stubs , member } ) ;
2022-08-25 10:25:36 +03:00
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' ,
2022-08-25 18:57:50 +03:00
email : 'member@example.com' ,
2022-08-25 10:25:36 +03:00
id : 'abc' ,
2022-08-25 21:46:54 +03:00
geolocation : '{"country": "France"}' ,
2022-08-25 10:25:36 +03:00
created _at : '2022-08-01T07:30:39.882Z'
} ;
tier = {
name : 'Test Tier'
} ;
subscription = {
2022-09-09 17:23:43 +03:00
amount : 5000 ,
currency : 'USD' ,
interval : 'month' ,
cancelAt : '2024-08-01T07:30:39.882Z' ,
canceledAt : '2022-08-05T07:30:39.882Z'
2022-08-25 10:25:36 +03:00
} ;
} ) ;
it ( 'sends paid subscription cancel alert' , async function ( ) {
2022-09-09 17:23:43 +03:00
await service . emails . notifyPaidSubscriptionCanceled ( { member , tier , subscription : {
... subscription ,
cancellationReason : 'Changed my mind!'
} } , options ) ;
2022-08-25 10:25:36 +03:00
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 (
2022-09-09 17:23:43 +03:00
sinon . match . has ( 'html' , sinon . match ( '1 Aug 2024' ) )
2022-08-25 10:25:36 +03:00
) . should . be . true ( ) ;
mailStub . calledWith (
sinon . match . has ( 'html' , 'Offer' )
) . should . be . false ( ) ;
2022-08-25 22:49:55 +03:00
mailStub . calledWith (
2022-10-07 13:31:05 +03:00
sinon . match . has ( 'html' , sinon . match ( 'Reason: Changed my mind!' ) )
2022-08-25 22:49:55 +03:00
) . should . be . true ( ) ;
2022-08-25 23:18:47 +03:00
mailStub . calledWith (
sinon . match . has ( 'html' , sinon . match ( 'Cancellation reason' ) )
) . should . be . true ( ) ;
2022-08-25 22:49:55 +03:00
} ) ;
it ( 'sends paid subscription cancel alert without reason' , async function ( ) {
2022-09-09 17:23:43 +03:00
await service . emails . notifyPaidSubscriptionCanceled ( { member , tier , subscription } , options ) ;
2022-08-25 22:49:55 +03:00
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 (
2022-09-09 17:23:43 +03:00
sinon . match . has ( 'html' , sinon . match ( '1 Aug 2024' ) )
2022-08-25 22:49:55 +03:00
) . should . be . true ( ) ;
mailStub . calledWith (
sinon . match . has ( 'html' , sinon . match ( 'Reason: ' ) )
) . should . be . false ( ) ;
2022-08-25 23:18:47 +03:00
mailStub . calledWith (
sinon . match . has ( 'html' , sinon . match ( 'Cancellation reason' ) )
) . should . be . false ( ) ;
2022-10-07 13:31:05 +03:00
// check preview text
mailStub . calledWith (
sinon . match . has ( 'html' , sinon . match ( 'A paid member has just canceled their subscription.' ) )
) . should . be . true ( ) ;
2022-08-25 10:25:36 +03:00
} ) ;
} ) ;
2023-02-23 12:20:13 +03:00
describe ( 'notifyMilestoneReceived' , function ( ) {
2023-03-21 16:39:40 +03:00
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. That’ s 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 — 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 ( ) {
2023-02-23 12:20:13 +03:00
const milestone = {
type : 'members' ,
value : 25000
} ;
await service . emails . notifyMilestoneReceived ( { milestone } ) ;
2023-03-21 16:39:40 +03:00
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 ( ) ;
2023-02-23 12:20:13 +03:00
mailStub . called . should . be . false ( ) ;
} ) ;
} ) ;
2023-03-28 22:06:05 +03:00
2023-08-07 16:36:59 +03:00
describe ( 'notifyDonationReceived' , function ( ) {
it ( 'send donation email' , async function ( ) {
const donationPaymentEvent = {
amount : 1500 ,
currency : 'eur' ,
name : 'Simon' ,
email : 'simon@example.com'
} ;
await service . emails . notifyDonationReceived ( { donationPaymentEvent } ) ;
getEmailAlertUsersStub . calledWith ( 'donation' ) . should . be . true ( ) ;
mailStub . calledOnce . should . be . true ( ) ;
mailStub . calledWith (
2023-08-10 20:35:42 +03:00
sinon . match . has ( 'html' , sinon . match ( 'One-time payment received: €15.00 from Simon' ) )
2023-08-07 16:36:59 +03:00
) . should . be . true ( ) ;
} ) ;
} ) ;
2023-03-28 22:06:05 +03:00
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/ ) ;
} ) ;
} ) ;
2022-08-25 10:25:36 +03:00
} ) ;
} ) ;