2022-02-14 16:13:54 +03:00
const { agentProvider , mockManager , fixtureManager , matchers } = require ( '../../utils/e2e-framework' ) ;
2022-03-01 14:17:13 +03:00
const { anyEtag , anyObjectId , anyUuid , anyISODateTime , anyISODate , anyString , anyArray , anyLocationFor , anyErrorId } = matchers ;
2022-05-04 14:51:35 +03:00
const ObjectId = require ( 'bson-objectid' ) ;
2022-02-14 16:13:54 +03:00
2022-02-16 21:33:46 +03:00
const assert = require ( 'assert' ) ;
2022-02-16 16:23:59 +03:00
const nock = require ( 'nock' ) ;
2022-02-14 16:13:54 +03:00
const should = require ( 'should' ) ;
const sinon = require ( 'sinon' ) ;
const testUtils = require ( '../../utils' ) ;
const Papa = require ( 'papaparse' ) ;
2022-02-18 20:20:42 +03:00
const models = require ( '../../../core/server/models' ) ;
2022-04-06 11:40:16 +03:00
async function assertMemberEvents ( { eventType , memberId , asserts } ) {
const events = await models [ eventType ] . where ( 'member_id' , memberId ) . fetchAll ( ) ;
2022-04-27 18:04:55 +03:00
const eventsJSON = events . map ( e => e . toJSON ( ) ) ;
2022-05-11 19:56:03 +03:00
2022-04-27 18:04:55 +03:00
// Order shouldn't matter here
for ( const a of asserts ) {
eventsJSON . should . matchAny ( a ) ;
}
2022-04-06 11:40:16 +03:00
assert . equal ( events . length , asserts . length , ` Only ${ asserts . length } ${ eventType } should have been added. ` ) ;
}
2022-02-21 15:18:02 +03:00
2022-04-12 14:22:26 +03:00
async function assertSubscription ( subscriptionId , asserts ) {
// eslint-disable-next-line dot-notation
const subscription = await models [ 'StripeCustomerSubscription' ] . where ( 'subscription_id' , subscriptionId ) . fetch ( { require : true } ) ;
// We use the native toJSON to prevent calling the overriden serialize method
models . Base . Model . prototype . serialize . call ( subscription ) . should . match ( asserts ) ;
}
2022-04-06 11:40:16 +03:00
async function getPaidProduct ( ) {
2022-04-26 14:21:31 +03:00
return await models . Product . findOne ( { type : 'paid' } ) ;
}
async function getNewsletters ( ) {
return ( await models . Newsletter . findAll ( { filter : 'status:active' } ) ) . models ;
2022-02-18 20:20:42 +03:00
}
2022-04-22 15:39:27 +03:00
const newsletterSnapshot = {
id : anyObjectId ,
2022-04-27 16:50:25 +03:00
uuid : anyUuid ,
2022-04-22 15:39:27 +03:00
created _at : anyISODateTime ,
updated _at : anyISODateTime
} ;
2022-04-26 14:21:31 +03:00
function buildMemberWithoutIncludesSnapshot ( options ) {
return {
id : anyObjectId ,
uuid : anyUuid ,
created _at : anyISODateTime ,
updated _at : anyISODateTime ,
newsletters : new Array ( options . newsletters ) . fill ( newsletterSnapshot )
} ;
}
2022-02-16 21:33:46 +03:00
2022-04-27 18:04:55 +03:00
function buildMemberWithIncludesSnapshot ( options ) {
return {
id : anyObjectId ,
uuid : anyUuid ,
created _at : anyISODateTime ,
updated _at : anyISODateTime ,
newsletters : new Array ( options . newsletters ) . fill ( newsletterSnapshot ) ,
subscriptions : anyArray ,
labels : anyArray
} ;
}
2022-02-16 21:33:46 +03:00
const memberMatcherShallowIncludes = {
2022-04-04 15:45:30 +03:00
id : anyObjectId ,
uuid : anyUuid ,
created _at : anyISODateTime ,
updated _at : anyISODateTime ,
subscriptions : anyArray ,
labels : anyArray ,
newsletters : anyArray
} ;
2022-05-11 19:56:03 +03:00
const memberMatcherShallowIncludesWithTiers = {
... memberMatcherShallowIncludes ,
tiers : anyArray
} ;
2022-02-14 16:13:54 +03:00
let agent ;
2022-02-16 21:33:46 +03:00
describe ( 'Members API without Stripe' , function ( ) {
before ( async function ( ) {
agent = await agentProvider . getAdminAPIAgent ( ) ;
2022-02-18 11:35:03 +03:00
await fixtureManager . init ( ) ;
2022-02-16 21:33:46 +03:00
await agent . loginAsOwner ( ) ;
2022-02-18 11:35:03 +03:00
await agent
. delete ( '/settings/stripe/connect/' )
2022-04-25 20:17:35 +03:00
. expectStatus ( 204 ) ;
2022-02-18 11:35:03 +03:00
} ) ;
2022-02-16 21:33:46 +03:00
beforeEach ( function ( ) {
mockManager . mockMail ( ) ;
} ) ;
afterEach ( function ( ) {
mockManager . restore ( ) ;
} ) ;
it ( 'Add should fail when comped flag is passed in but Stripe is not enabled' , async function ( ) {
const newMember = {
email : 'memberTestAdd@test.com' ,
comped : true
} ;
await agent
. post ( ` members/ ` )
. body ( { members : [ newMember ] } )
. expectStatus ( 422 )
. matchHeaderSnapshot ( {
etag : anyEtag
} )
. matchBodySnapshot ( {
errors : [ {
id : anyErrorId
} ]
} ) ;
} ) ;
} ) ;
2022-02-14 16:13:54 +03:00
describe ( 'Members API' , function ( ) {
2022-04-26 14:21:31 +03:00
let newsletters ;
2022-02-14 16:13:54 +03:00
before ( async function ( ) {
agent = await agentProvider . getAdminAPIAgent ( ) ;
2022-04-26 14:21:31 +03:00
await fixtureManager . init ( 'newsletters' , 'members:newsletters' ) ;
2022-02-14 16:13:54 +03:00
await agent . loginAsOwner ( ) ;
2022-04-26 14:21:31 +03:00
newsletters = await getNewsletters ( ) ;
2022-02-14 16:13:54 +03:00
} ) ;
beforeEach ( function ( ) {
2022-02-16 16:23:59 +03:00
mockManager . mockStripe ( ) ;
2022-02-16 21:33:46 +03:00
mockManager . mockMail ( ) ;
2022-02-14 16:13:54 +03:00
} ) ;
afterEach ( function ( ) {
mockManager . restore ( ) ;
} ) ;
2022-02-17 17:17:48 +03:00
// List Members
2022-02-14 16:13:54 +03:00
it ( 'Can browse' , async function ( ) {
await agent
. get ( '/members/' )
. expectStatus ( 200 )
. matchBodySnapshot ( {
2022-03-18 22:40:11 +03:00
members : new Array ( 8 ) . fill ( memberMatcherShallowIncludes )
2022-02-14 16:13:54 +03:00
} )
. matchHeaderSnapshot ( {
etag : anyEtag
} ) ;
} ) ;
it ( 'Can browse with filter' , async function ( ) {
await agent
. get ( '/members/?filter=label:label-1' )
. expectStatus ( 200 )
. matchBodySnapshot ( {
2022-03-18 22:40:11 +03:00
members : new Array ( 1 ) . fill ( memberMatcherShallowIncludes )
2022-02-14 16:13:54 +03:00
} )
. matchHeaderSnapshot ( {
etag : anyEtag
} ) ;
} ) ;
it ( 'Can browse with search' , async function ( ) {
await agent
. get ( '/members/?search=member1' )
. expectStatus ( 200 )
. matchBodySnapshot ( {
2022-03-18 22:40:11 +03:00
members : new Array ( 1 ) . fill ( memberMatcherShallowIncludes )
2022-02-14 16:13:54 +03:00
} )
. matchHeaderSnapshot ( {
etag : anyEtag
} ) ;
} ) ;
it ( 'Can filter by paid status' , async function ( ) {
await agent
. get ( '/members/?filter=status:paid' )
. expectStatus ( 200 )
. matchBodySnapshot ( {
2022-03-18 22:40:11 +03:00
members : new Array ( 5 ) . fill ( memberMatcherShallowIncludes )
2022-02-14 16:13:54 +03:00
} )
. matchHeaderSnapshot ( {
etag : anyEtag
} ) ;
} ) ;
2022-03-09 16:02:17 +03:00
it ( 'Can filter using contains operators' , async function ( ) {
await agent
. get ( ` /members/?filter=name:~'Venkman' ` )
. expectStatus ( 200 )
. matchBodySnapshot ( {
2022-03-18 22:40:11 +03:00
members : new Array ( 1 ) . fill ( memberMatcherShallowIncludes )
2022-03-09 16:02:17 +03:00
} )
. matchHeaderSnapshot ( {
etag : anyEtag
} ) ;
} ) ;
2022-03-08 14:29:51 +03:00
it ( 'Can ignore any unknown includes' , async function ( ) {
await agent
. get ( '/members/?filter=status:paid&include=emailRecipients' )
. expectStatus ( 200 )
. matchBodySnapshot ( {
2022-03-18 22:40:11 +03:00
members : new Array ( 5 ) . fill ( memberMatcherShallowIncludes )
2022-03-08 14:29:51 +03:00
} )
. matchHeaderSnapshot ( {
etag : anyEtag
} ) ;
} ) ;
2022-02-17 17:17:48 +03:00
it ( 'Can order by email_open_rate' , async function ( ) {
await agent
. get ( 'members/?order=email_open_rate%20desc' )
. expectStatus ( 200 )
. matchHeaderSnapshot ( {
etag : anyEtag ,
'content-length' : anyString
} )
. matchBodySnapshot ( {
members : new Array ( 8 ) . fill ( memberMatcherShallowIncludes )
} )
. expect ( ( { body } ) => {
const { members } = body ;
assert . equal ( members [ 0 ] . email _open _rate > members [ 1 ] . email _open _rate , true , 'Expected the first member to have a greater open rate than the second.' ) ;
} ) ;
await agent
. get ( 'members/?order=email_open_rate%20asc' )
. expectStatus ( 200 )
. matchHeaderSnapshot ( {
2022-02-17 17:56:16 +03:00
etag : anyEtag ,
'content-length' : anyString
2022-02-17 17:17:48 +03:00
} )
. matchBodySnapshot ( {
members : new Array ( 8 ) . fill ( memberMatcherShallowIncludes )
} )
. expect ( ( { body } ) => {
const { members } = body ;
assert . equal ( members [ 0 ] . email _open _rate < members [ 1 ] . email _open _rate , true , 'Expected the first member to have a smaller open rate than the second.' ) ;
} ) ;
} ) ;
2022-02-18 07:26:07 +03:00
it ( 'Search by case-insensitive name egg receives member with name Mr Egg' , async function ( ) {
2022-02-17 17:17:48 +03:00
await agent
. get ( 'members/?search=egg' )
. expectStatus ( 200 )
. matchBodySnapshot ( {
members : [ memberMatcherShallowIncludes ]
} )
. matchHeaderSnapshot ( {
etag : anyEtag
} ) ;
} ) ;
it ( 'Search by case-insensitive email MEMBER2 receives member with email member2@test.com' , async function ( ) {
await agent
. get ( 'members/?search=MEMBER2' )
. expectStatus ( 200 )
. matchBodySnapshot ( {
members : [ memberMatcherShallowIncludes ]
} )
. matchHeaderSnapshot ( {
etag : anyEtag
} ) ;
} ) ;
2022-02-18 07:26:07 +03:00
it ( 'Search for paid members retrieves member with email paid@test.com' , async function ( ) {
2022-02-17 17:17:48 +03:00
await agent
. get ( 'members/?search=egon&paid=true' )
. expectStatus ( 200 )
. matchBodySnapshot ( {
members : [ memberMatcherShallowIncludes ]
} )
. matchHeaderSnapshot ( {
etag : anyEtag
} ) ;
} ) ;
it ( 'Search for non existing member returns empty result set' , async function ( ) {
await agent
. get ( 'members/?search=do_not_exist' )
. expectStatus ( 200 )
. matchHeaderSnapshot ( {
etag : anyEtag
} )
. matchBodySnapshot ( {
members : [ ]
} ) ;
} ) ;
// Read a member
2022-02-14 16:13:54 +03:00
it ( 'Can read' , async function ( ) {
await agent
. get ( ` /members/ ${ testUtils . DataGenerator . Content . members [ 0 ] . id } / ` )
. expectStatus ( 200 )
. matchBodySnapshot ( {
2022-03-18 22:40:11 +03:00
members : new Array ( 1 ) . fill ( memberMatcherShallowIncludes )
2022-02-14 16:13:54 +03:00
} )
. matchHeaderSnapshot ( {
etag : anyEtag
} ) ;
} ) ;
it ( 'Can read and include email_recipients' , async function ( ) {
await agent
. get ( ` /members/ ${ testUtils . DataGenerator . Content . members [ 0 ] . id } /?include=email_recipients ` )
. expectStatus ( 200 )
. matchBodySnapshot ( {
2022-03-18 22:40:11 +03:00
members : new Array ( 1 ) . fill ( memberMatcherShallowIncludes )
2022-02-14 16:13:54 +03:00
} )
. matchHeaderSnapshot ( {
etag : anyEtag
} ) ;
} ) ;
2022-05-11 19:56:03 +03:00
it ( 'Can read and include tiers' , async function ( ) {
await agent
. get ( ` /members/ ${ testUtils . DataGenerator . Content . members [ 0 ] . id } /?include=tiers ` )
. expectStatus ( 200 )
. matchBodySnapshot ( {
members : new Array ( 1 ) . fill ( memberMatcherShallowIncludesWithTiers )
} )
. matchHeaderSnapshot ( {
etag : anyEtag
} ) ;
} ) ;
2022-02-17 17:17:48 +03:00
// Create a member
2022-02-14 16:13:54 +03:00
it ( 'Can add' , async function ( ) {
const member = {
name : 'test' ,
email : 'memberTestAdd@test.com' ,
note : 'test note' ,
2022-04-26 14:21:31 +03:00
newsletters : [ ] ,
2022-02-14 16:13:54 +03:00
labels : [ 'test-label' ]
} ;
2022-04-06 11:40:16 +03:00
const { body } = await agent
2022-02-14 16:13:54 +03:00
. post ( ` /members/ ` )
. body ( { members : [ member ] } )
. expectStatus ( 201 )
. matchBodySnapshot ( {
2022-03-18 22:40:11 +03:00
members : new Array ( 1 ) . fill ( memberMatcherShallowIncludes )
2022-02-14 16:13:54 +03:00
} )
. matchHeaderSnapshot ( {
etag : anyEtag ,
2022-02-16 15:50:58 +03:00
location : anyLocationFor ( 'members' )
2022-02-14 16:13:54 +03:00
} ) ;
2022-04-06 11:40:16 +03:00
const newMember = body . members [ 0 ] ;
2022-02-14 16:13:54 +03:00
await agent
. post ( ` /members/ ` )
. body ( { members : [ member ] } )
. expectStatus ( 422 ) ;
2022-02-18 20:20:42 +03:00
2022-04-06 11:40:16 +03:00
await assertMemberEvents ( {
2022-02-18 20:20:42 +03:00
eventType : 'MemberStatusEvent' ,
2022-04-06 11:40:16 +03:00
memberId : newMember . id ,
asserts : [
{
2022-04-07 06:00:00 +03:00
from _status : null ,
2022-04-06 11:40:16 +03:00
to _status : 'free'
}
]
2022-02-18 20:20:42 +03:00
} ) ;
2022-02-14 16:13:54 +03:00
} ) ;
2022-04-26 14:21:31 +03:00
it ( 'Can add and send a signup confirmation email' , async function ( ) {
const member = {
name : 'Send Me Confirmation' ,
email : 'member_getting_confirmation@test.com' ,
newsletters : [
newsletters [ 0 ] ,
newsletters [ 1 ]
]
} ;
const { body } = await agent
. post ( '/members/?send_email=true&email_type=signup' )
. body ( { members : [ member ] } )
. expectStatus ( 201 )
. matchBodySnapshot ( {
members : [
buildMemberWithoutIncludesSnapshot ( {
newsletters : 2
} )
]
} )
. matchHeaderSnapshot ( {
etag : anyEtag ,
location : anyString
} ) ;
const newMember = body . members [ 0 ] ;
mockManager . assert . sentEmail ( {
subject : '🙌 Complete your sign up to Ghost!' ,
to : 'member_getting_confirmation@test.com'
} ) ;
await assertMemberEvents ( {
eventType : 'MemberStatusEvent' ,
memberId : newMember . id ,
asserts : [
{
from _status : null ,
to _status : 'free'
}
]
} ) ;
await assertMemberEvents ( {
eventType : 'MemberSubscribeEvent' ,
memberId : newMember . id ,
asserts : [
{
subscribed : true ,
source : 'admin' ,
newsletter _id : newsletters [ 0 ] . id
} ,
{
subscribed : true ,
source : 'admin' ,
newsletter _id : newsletters [ 1 ] . id
}
]
} ) ;
2022-02-17 17:17:48 +03:00
// @TODO: do we really need to delete this member here?
await agent
. delete ( ` members/ ${ body . members [ 0 ] . id } / ` )
. matchHeaderSnapshot ( {
etag : anyEtag
} )
. expectStatus ( 204 ) ;
2022-02-18 20:20:42 +03:00
2022-04-27 18:04:55 +03:00
// There should be no MemberSubscribeEvent remaining.
await assertMemberEvents ( {
eventType : 'MemberSubscribeEvent' ,
memberId : newMember . id ,
asserts : [ ]
} ) ;
2022-02-17 17:17:48 +03:00
} ) ;
it ( 'Add should fail when passing incorrect email_type query parameter' , async function ( ) {
const newMember = {
name : 'test' ,
email : 'memberTestAdd@test.com'
} ;
2022-04-27 18:04:55 +03:00
const statusEventsBefore = await models . MemberStatusEvent . findAll ( ) ;
2022-02-17 17:17:48 +03:00
await agent
. post ( ` members/?send_email=true&email_type=lel ` )
. body ( { members : [ newMember ] } )
. expectStatus ( 422 )
. matchHeaderSnapshot ( {
etag : anyEtag
} )
. matchBodySnapshot ( {
errors : [ {
id : anyErrorId
} ]
} ) ;
2022-02-18 20:20:42 +03:00
const statusEvents = await models . MemberStatusEvent . findAll ( ) ;
2022-04-27 18:04:55 +03:00
assert . equal ( statusEvents . models . length , statusEventsBefore . models . length , 'No MemberStatusEvent should have been added after failing to create a subscription.' ) ;
2022-02-17 17:17:48 +03:00
} ) ;
// Edit a member
2022-04-06 11:40:16 +03:00
it ( 'Can add complimentary subscription (out of date)' , async function ( ) {
2022-02-14 16:13:54 +03:00
const stripeService = require ( '../../../core/server/services/stripe' ) ;
const fakePrice = {
id : 'price_1' ,
product : '' ,
active : true ,
nickname : 'Complimentary' ,
unit _amount : 0 ,
2022-04-12 14:22:26 +03:00
currency : 'usd' ,
2022-02-14 16:13:54 +03:00
type : 'recurring' ,
recurring : {
interval : 'year'
}
} ;
const fakeSubscription = {
id : 'sub_1' ,
customer : 'cus_1' ,
status : 'active' ,
cancel _at _period _end : false ,
metadata : { } ,
current _period _end : Date . now ( ) / 1000 ,
start _date : Date . now ( ) / 1000 ,
plan : fakePrice ,
items : {
data : [ {
price : fakePrice
} ]
}
} ;
sinon . stub ( stripeService . api , 'createCustomer' ) . callsFake ( async function ( data ) {
return {
id : 'cus_1' ,
email : data . email
} ;
} ) ;
sinon . stub ( stripeService . api , 'createPrice' ) . resolves ( fakePrice ) ;
sinon . stub ( stripeService . api , 'createSubscription' ) . resolves ( fakeSubscription ) ;
sinon . stub ( stripeService . api , 'getSubscription' ) . resolves ( fakeSubscription ) ;
const initialMember = {
name : 'Name' ,
email : 'compedtest@test.com' ,
2022-04-26 14:21:31 +03:00
newsletters : [ newsletters [ 0 ] ]
2022-02-14 16:13:54 +03:00
} ;
const compedPayload = {
comped : true
} ;
const { body } = await agent
. post ( ` /members/ ` )
. body ( { members : [ initialMember ] } )
. expectStatus ( 201 )
. matchBodySnapshot ( {
2022-03-18 22:40:11 +03:00
members : new Array ( 1 ) . fill ( memberMatcherShallowIncludes )
2022-02-14 16:13:54 +03:00
} )
. matchHeaderSnapshot ( {
etag : anyEtag ,
2022-02-16 15:50:58 +03:00
location : anyLocationFor ( 'members' )
2022-02-14 16:13:54 +03:00
} ) ;
const newMember = body . members [ 0 ] ;
const { body : body2 } = await agent
. put ( ` /members/ ${ newMember . id } / ` )
. body ( { members : [ compedPayload ] } )
. expectStatus ( 200 )
. matchBodySnapshot ( {
2022-03-18 22:40:11 +03:00
members : new Array ( 1 ) . fill ( memberMatcherShallowIncludes )
2022-02-14 16:13:54 +03:00
} )
. matchHeaderSnapshot ( {
etag : anyEtag
} ) ;
2022-02-18 20:20:42 +03:00
2022-04-06 11:40:16 +03:00
await assertMemberEvents ( {
2022-02-18 20:20:42 +03:00
eventType : 'MemberStatusEvent' ,
2022-04-06 11:40:16 +03:00
memberId : newMember . id ,
asserts : [ {
2022-02-18 20:20:42 +03:00
from _status : null ,
to _status : 'free'
2022-04-06 11:40:16 +03:00
} ]
} ) ;
await assertMemberEvents ( {
eventType : 'MemberSubscribeEvent' ,
memberId : newMember . id ,
asserts : [ {
subscribed : true ,
2022-04-26 14:21:31 +03:00
source : 'admin' ,
newsletter _id : newsletters [ 0 ] . id
2022-04-06 11:40:16 +03:00
} ]
} ) ;
} ) ;
it ( 'Can add complimentary subscription by assigning a product to a member' , async function ( ) {
const initialMember = {
name : 'Name' ,
email : 'compedtest2@test.com' ,
2022-04-26 14:21:31 +03:00
newsletters : [ newsletters [ 0 ] ]
2022-04-06 11:40:16 +03:00
} ;
const { body } = await agent
. post ( ` /members/ ` )
. body ( { members : [ initialMember ] } )
. expectStatus ( 201 ) ;
const newMember = body . members [ 0 ] ;
assert . equal ( newMember . status , 'free' , 'A new member should have the free status' ) ;
const product = await getPaidProduct ( ) ;
const compedPayload = {
id : newMember . id ,
email : newMember . email ,
2022-05-11 19:56:03 +03:00
tiers : [
2022-04-06 11:40:16 +03:00
{
id : product . id
}
]
} ;
const { body : body2 } = await agent
. put ( ` /members/ ${ newMember . id } / ` )
. body ( { members : [ compedPayload ] } )
. expectStatus ( 200 ) ;
const updatedMember = body2 . members [ 0 ] ;
assert . equal ( updatedMember . status , 'comped' , 'A comped member should have the comped status' ) ;
assert . equal ( updatedMember . products . length , 1 , 'The member should have one product' ) ;
2022-05-11 19:56:03 +03:00
assert . equal ( updatedMember . tiers . length , 1 , 'The member should have one product' ) ;
2022-04-06 11:40:16 +03:00
await assertMemberEvents ( {
eventType : 'MemberStatusEvent' ,
memberId : newMember . id ,
asserts : [
{
from _status : null ,
to _status : 'free'
} ,
{
from _status : 'free' ,
to _status : 'comped'
}
]
2022-02-18 20:20:42 +03:00
} ) ;
2022-04-06 11:40:16 +03:00
await assertMemberEvents ( {
2022-02-18 20:20:42 +03:00
eventType : 'MemberSubscribeEvent' ,
2022-04-06 11:40:16 +03:00
memberId : newMember . id ,
asserts : [ {
2022-02-18 20:20:42 +03:00
subscribed : true ,
2022-04-26 14:21:31 +03:00
source : 'admin' ,
newsletter _id : newsletters [ 0 ] . id
2022-04-06 11:40:16 +03:00
} ]
} ) ;
await assertMemberEvents ( {
eventType : 'MemberPaidSubscriptionEvent' ,
memberId : newMember . id ,
asserts : [ ]
} ) ;
} ) ;
it ( 'Can end a complimentary subscription by removing a product from a member' , async function ( ) {
const product = await getPaidProduct ( ) ;
const initialMember = {
name : 'Name' ,
email : 'compedtest3@test.com' ,
2022-04-26 14:21:31 +03:00
newsletters : [ newsletters [ 0 ] ] ,
2022-05-11 19:56:03 +03:00
tiers : [
2022-04-06 11:40:16 +03:00
{
id : product . id
}
]
} ;
2022-04-07 06:00:00 +03:00
2022-04-06 11:40:16 +03:00
const { body } = await agent
. post ( ` /members/ ` )
. body ( { members : [ initialMember ] } )
. expectStatus ( 201 ) ;
const newMember = body . members [ 0 ] ;
assert . equal ( newMember . status , 'comped' , 'The new member should have the comped status' ) ;
2022-05-11 19:56:03 +03:00
assert . equal ( newMember . tiers . length , 1 , 'The member should have 1 product' ) ;
2022-04-06 11:40:16 +03:00
// Remove it
const removePayload = {
id : newMember . id ,
email : newMember . email ,
2022-05-11 19:56:03 +03:00
tiers : [ ]
2022-04-06 11:40:16 +03:00
} ;
const { body : body2 } = await agent
. put ( ` /members/ ${ newMember . id } / ` )
. body ( { members : [ removePayload ] } )
. expectStatus ( 200 ) ;
const updatedMember = body2 . members [ 0 ] ;
assert . equal ( updatedMember . status , 'free' , 'The member should have the free status' ) ;
assert . equal ( updatedMember . products . length , 0 , 'The member should have 0 products' ) ;
2022-05-11 19:56:03 +03:00
assert . equal ( updatedMember . tiers . length , 0 , 'The member should have 0 products' ) ;
2022-04-06 11:40:16 +03:00
await assertMemberEvents ( {
eventType : 'MemberStatusEvent' ,
memberId : newMember . id ,
asserts : [
{
from _status : null ,
to _status : 'comped'
} ,
{
from _status : 'comped' ,
to _status : 'free'
}
]
} ) ;
await assertMemberEvents ( {
eventType : 'MemberSubscribeEvent' ,
memberId : newMember . id ,
asserts : [
{
subscribed : true ,
2022-04-26 14:21:31 +03:00
source : 'admin' ,
newsletter _id : newsletters [ 0 ] . id
2022-04-06 11:40:16 +03:00
}
]
} ) ;
await assertMemberEvents ( {
eventType : 'MemberPaidSubscriptionEvent' ,
memberId : newMember . id ,
asserts : [ ]
} ) ;
} ) ;
it ( 'Can create a new member with a product (complementary)' , async function ( ) {
const product = await getPaidProduct ( ) ;
const initialMember = {
name : 'Name' ,
email : 'compedtest4@test.com' ,
subscribed : true ,
2022-04-26 14:21:31 +03:00
newsletters : [ newsletters [ 0 ] ] ,
2022-05-11 19:56:03 +03:00
tiers : [
2022-04-06 11:40:16 +03:00
{
id : product . id
}
]
} ;
const { body } = await agent
. post ( ` /members/ ` )
. body ( { members : [ initialMember ] } )
. expectStatus ( 201 )
. matchBodySnapshot ( {
members : new Array ( 1 ) . fill ( {
id : anyObjectId ,
uuid : anyUuid ,
created _at : anyISODateTime ,
updated _at : anyISODateTime ,
labels : anyArray ,
subscriptions : anyArray ,
products : new Array ( 1 ) . fill ( {
id : anyObjectId ,
monthly _price _id : anyObjectId ,
yearly _price _id : anyObjectId ,
created _at : anyISODateTime ,
updated _at : anyISODateTime
2022-04-26 14:21:31 +03:00
} ) ,
2022-05-11 19:56:03 +03:00
tiers : new Array ( 1 ) . fill ( {
id : anyObjectId ,
monthly _price _id : anyObjectId ,
yearly _price _id : anyObjectId ,
created _at : anyISODateTime ,
updated _at : anyISODateTime
} ) ,
2022-04-26 14:21:31 +03:00
newsletters : new Array ( 1 ) . fill ( newsletterSnapshot )
2022-04-06 11:40:16 +03:00
} )
} )
. matchHeaderSnapshot ( {
etag : anyEtag ,
location : anyLocationFor ( 'members' )
} ) ;
const newMember = body . members [ 0 ] ;
assert . equal ( newMember . status , 'comped' , 'The newly imported member should have the comped status' ) ;
await assertMemberEvents ( {
eventType : 'MemberStatusEvent' ,
memberId : newMember . id ,
asserts : [ {
from _status : null ,
to _status : 'comped'
} ]
} ) ;
await assertMemberEvents ( {
eventType : 'MemberSubscribeEvent' ,
memberId : newMember . id ,
asserts : [ {
subscribed : true ,
source : 'admin'
} ]
} ) ;
await assertMemberEvents ( {
eventType : 'MemberPaidSubscriptionEvent' ,
memberId : newMember . id ,
asserts : [ ]
} ) ;
} ) ;
it ( 'Can create a member with an existing complimentary subscription' , async function ( ) {
const fakePrice = {
id : 'price_1' ,
product : '' ,
active : true ,
nickname : 'Complimentary' ,
unit _amount : 0 ,
2022-04-12 14:22:26 +03:00
currency : 'usd' ,
2022-04-06 11:40:16 +03:00
type : 'recurring' ,
recurring : {
interval : 'year'
}
} ;
const fakeSubscription = {
id : 'sub_1' ,
customer : 'cus_1234' ,
status : 'active' ,
cancel _at _period _end : false ,
metadata : { } ,
current _period _end : Date . now ( ) / 1000 + 1000 ,
start _date : Date . now ( ) / 1000 ,
plan : fakePrice ,
items : {
data : [ {
price : fakePrice
} ]
2022-02-18 20:20:42 +03:00
}
2022-04-06 11:40:16 +03:00
} ;
const fakeCustomer = {
id : 'cus_1234' ,
name : 'Test Member' ,
email : 'create-member-comped-test@email.com' ,
subscriptions : {
type : 'list' ,
data : [ fakeSubscription ]
}
} ;
nock ( 'https://api.stripe.com' )
. persist ( )
. get ( /v1\/.*/ )
. reply ( ( uri , body ) => {
const [ match , resource , id ] = uri . match ( /\/?v1\/(\w+)\/?(\w+)/ ) || [ null ] ;
if ( ! match ) {
return [ 500 ] ;
}
if ( resource === 'customers' ) {
return [ 200 , fakeCustomer ] ;
}
if ( resource === 'subscriptions' ) {
return [ 200 , fakeSubscription ] ;
}
} ) ;
2022-04-07 06:00:00 +03:00
2022-04-06 11:40:16 +03:00
const initialMember = {
name : fakeCustomer . name ,
email : fakeCustomer . email ,
subscribed : true ,
2022-04-26 14:21:31 +03:00
newsletters : [ newsletters [ 0 ] ] ,
2022-04-06 11:40:16 +03:00
stripe _customer _id : fakeCustomer . id
} ;
const { body } = await agent
. post ( ` /members/ ` )
. body ( { members : [ initialMember ] } )
. expectStatus ( 201 )
. matchBodySnapshot ( {
members : new Array ( 1 ) . fill ( {
id : anyObjectId ,
uuid : anyUuid ,
created _at : anyISODateTime ,
updated _at : anyISODateTime ,
labels : anyArray ,
subscriptions : anyArray ,
2022-04-26 14:21:31 +03:00
products : anyArray ,
2022-05-11 19:56:03 +03:00
tiers : anyArray ,
2022-04-26 14:21:31 +03:00
newsletters : new Array ( 1 ) . fill ( newsletterSnapshot )
2022-04-06 11:40:16 +03:00
} )
} )
. matchHeaderSnapshot ( {
etag : anyEtag ,
location : anyLocationFor ( 'members' )
} ) ;
const newMember = body . members [ 0 ] ;
assert . equal ( newMember . status , 'comped' , 'The created member should have the comped status' ) ;
await assertMemberEvents ( {
eventType : 'MemberStatusEvent' ,
memberId : newMember . id ,
asserts : [
{
from _status : null ,
to _status : 'free'
} ,
{
from _status : 'free' ,
to _status : 'comped'
}
]
} ) ;
await assertMemberEvents ( {
eventType : 'MemberSubscribeEvent' ,
memberId : newMember . id ,
asserts : [
{
subscribed : true ,
source : 'admin'
}
]
} ) ;
await assertMemberEvents ( {
eventType : 'MemberPaidSubscriptionEvent' ,
memberId : newMember . id ,
asserts : [ {
mrr _delta : 0
} ]
} ) ;
} ) ;
2022-04-21 13:06:06 +03:00
let memberWithPaidSubscription ;
2022-04-06 11:40:16 +03:00
it ( 'Can create a member with an existing paid subscription' , async function ( ) {
const fakePrice = {
id : 'price_1' ,
2022-04-12 14:22:26 +03:00
product : 'product_1234' ,
2022-04-06 11:40:16 +03:00
active : true ,
nickname : 'Paid' ,
unit _amount : 1200 ,
2022-04-12 14:22:26 +03:00
currency : 'usd' ,
2022-04-06 11:40:16 +03:00
type : 'recurring' ,
recurring : {
interval : 'year'
}
} ;
const fakeSubscription = {
id : 'sub_987623' ,
customer : 'cus_12345' ,
status : 'active' ,
cancel _at _period _end : false ,
metadata : { } ,
current _period _end : Date . now ( ) / 1000 + 1000 ,
start _date : Date . now ( ) / 1000 ,
plan : fakePrice ,
items : {
data : [ {
2022-04-12 14:22:26 +03:00
id : 'item_123' ,
2022-04-06 11:40:16 +03:00
price : fakePrice
} ]
}
} ;
const fakeCustomer = {
id : 'cus_12345' ,
name : 'Test Member' ,
email : 'create-member-paid-test@email.com' ,
subscriptions : {
type : 'list' ,
data : [ fakeSubscription ]
}
} ;
nock ( 'https://api.stripe.com' )
. persist ( )
. get ( /v1\/.*/ )
. reply ( ( uri , body ) => {
const [ match , resource , id ] = uri . match ( /\/?v1\/(\w+)\/?(\w+)/ ) || [ null ] ;
if ( ! match ) {
return [ 500 ] ;
}
if ( resource === 'customers' ) {
return [ 200 , fakeCustomer ] ;
}
if ( resource === 'subscriptions' ) {
return [ 200 , fakeSubscription ] ;
}
} ) ;
2022-04-07 06:00:00 +03:00
2022-04-06 11:40:16 +03:00
const initialMember = {
name : fakeCustomer . name ,
email : fakeCustomer . email ,
subscribed : true ,
2022-04-26 14:21:31 +03:00
newsletters : [ newsletters [ 0 ] ] ,
2022-04-06 11:40:16 +03:00
stripe _customer _id : fakeCustomer . id
} ;
const { body } = await agent
. post ( ` /members/ ` )
. body ( { members : [ initialMember ] } )
. expectStatus ( 201 )
. matchBodySnapshot ( {
members : new Array ( 1 ) . fill ( {
id : anyObjectId ,
uuid : anyUuid ,
created _at : anyISODateTime ,
updated _at : anyISODateTime ,
labels : anyArray ,
subscriptions : anyArray ,
2022-04-26 14:21:31 +03:00
products : anyArray ,
2022-05-11 19:56:03 +03:00
tiers : anyArray ,
2022-04-26 14:21:31 +03:00
newsletters : new Array ( 1 ) . fill ( newsletterSnapshot )
2022-04-06 11:40:16 +03:00
} )
} )
. matchHeaderSnapshot ( {
etag : anyEtag ,
location : anyLocationFor ( 'members' )
} ) ;
const newMember = body . members [ 0 ] ;
2022-04-21 13:06:06 +03:00
2022-04-06 11:40:16 +03:00
assert . equal ( newMember . status , 'paid' , 'The created member should have the paid status' ) ;
2022-04-12 14:22:26 +03:00
assert . equal ( newMember . subscriptions . length , 1 , 'The member should have a single subscription' ) ;
assert . equal ( newMember . subscriptions [ 0 ] . id , fakeSubscription . id , 'The returned subscription should have an ID assigned' ) ;
2022-04-06 11:40:16 +03:00
await assertMemberEvents ( {
eventType : 'MemberStatusEvent' ,
memberId : newMember . id ,
asserts : [
{
from _status : null ,
to _status : 'free'
} , {
from _status : 'free' ,
to _status : 'paid'
}
]
} ) ;
await assertMemberEvents ( {
eventType : 'MemberSubscribeEvent' ,
memberId : newMember . id ,
asserts : [ {
subscribed : true ,
source : 'admin'
} ]
} ) ;
await assertMemberEvents ( {
eventType : 'MemberPaidSubscriptionEvent' ,
memberId : newMember . id ,
asserts : [
{
mrr _delta : 100
}
]
2022-02-18 20:20:42 +03:00
} ) ;
2022-04-12 14:22:26 +03:00
await assertSubscription ( fakeSubscription . id , {
subscription _id : fakeSubscription . id ,
status : 'active' ,
cancel _at _period _end : false ,
plan _amount : 1200 ,
plan _interval : 'year' ,
plan _currency : 'usd' ,
mrr : 100
} ) ;
2022-04-21 13:06:06 +03:00
// Save this member for the next test
memberWithPaidSubscription = newMember ;
} ) ;
it ( 'Returns an identical member format for read, edit and browse' , async function ( ) {
if ( ! memberWithPaidSubscription ) {
// Previous test failed
this . skip ( ) ;
}
// Check status has been updated to 'free' after cancelling
const { body : readBody } = await agent . get ( '/members/' + memberWithPaidSubscription . id + '/' ) ;
assert . equal ( readBody . members . length , 1 , 'The member was not found in read' ) ;
const readMember = readBody . members [ 0 ] ;
// Note that we explicitly need to ask to include products while browsing
2022-05-11 19:56:03 +03:00
const { body : browseBody } = await agent . get ( ` /members/?search= ${ memberWithPaidSubscription . email } &include=tiers ` ) ;
2022-04-21 13:06:06 +03:00
assert . equal ( browseBody . members . length , 1 , 'The member was not found in browse' ) ;
const browseMember = browseBody . members [ 0 ] ;
// Check for this member with a paid subscription that the body results for the patch, get and browse endpoints are 100% identical
should . deepEqual ( browseMember , readMember , 'Browsing a member returns a different format than reading a member' ) ;
should . deepEqual ( memberWithPaidSubscription , readMember , 'Editing a member returns a different format than reading a member' ) ;
2022-02-14 16:13:54 +03:00
} ) ;
it ( 'Can edit by id' , async function ( ) {
const memberToChange = {
name : 'change me' ,
email : 'member2Change@test.com' ,
note : 'initial note' ,
2022-04-26 14:21:31 +03:00
newsletters : [
newsletters [ 0 ]
]
2022-02-14 16:13:54 +03:00
} ;
const memberChanged = {
name : 'changed' ,
email : 'cantChangeMe@test.com' ,
note : 'edited note' ,
2022-04-26 14:21:31 +03:00
newsletters : [ ]
2022-02-14 16:13:54 +03:00
} ;
const { body } = await agent
. post ( ` /members/ ` )
. body ( { members : [ memberToChange ] } )
. expectStatus ( 201 )
. matchBodySnapshot ( {
2022-03-18 22:40:11 +03:00
members : new Array ( 1 ) . fill ( memberMatcherShallowIncludes )
2022-02-14 16:13:54 +03:00
} )
. matchHeaderSnapshot ( {
etag : anyEtag ,
2022-02-16 15:50:58 +03:00
location : anyLocationFor ( 'members' )
2022-02-14 16:13:54 +03:00
} ) ;
2022-04-06 11:40:16 +03:00
const newMember = body . members [ 0 ] ;
2022-02-14 16:13:54 +03:00
2022-04-06 11:40:16 +03:00
await assertMemberEvents ( {
2022-02-18 20:20:42 +03:00
eventType : 'MemberSubscribeEvent' ,
2022-04-06 11:40:16 +03:00
memberId : newMember . id ,
asserts : [ {
2022-02-18 20:20:42 +03:00
subscribed : true ,
2022-04-26 14:21:31 +03:00
source : 'admin' ,
newsletter _id : newsletters [ 0 ] . id
2022-04-06 11:40:16 +03:00
} ]
2022-02-18 20:20:42 +03:00
} ) ;
2022-04-06 11:40:16 +03:00
await assertMemberEvents ( {
2022-02-18 20:20:42 +03:00
eventType : 'MemberStatusEvent' ,
2022-04-06 11:40:16 +03:00
memberId : newMember . id ,
asserts : [ {
2022-02-18 20:20:42 +03:00
from _status : null ,
to _status : 'free'
2022-04-06 11:40:16 +03:00
} ]
2022-02-18 20:20:42 +03:00
} ) ;
2022-02-14 16:13:54 +03:00
await agent
. put ( ` /members/ ${ newMember . id } / ` )
. body ( { members : [ memberChanged ] } )
. expectStatus ( 200 )
. matchBodySnapshot ( {
2022-03-18 22:40:11 +03:00
members : new Array ( 1 ) . fill ( memberMatcherShallowIncludes )
2022-02-14 16:13:54 +03:00
} )
. matchHeaderSnapshot ( {
etag : anyEtag
} ) ;
2022-02-18 20:20:42 +03:00
2022-04-06 11:40:16 +03:00
await assertMemberEvents ( {
2022-02-18 20:20:42 +03:00
eventType : 'MemberEmailChangeEvent' ,
2022-04-06 11:40:16 +03:00
memberId : newMember . id ,
asserts : [ {
2022-02-18 20:20:42 +03:00
from _email : memberToChange . email ,
to _email : memberChanged . email
2022-04-06 11:40:16 +03:00
} ]
2022-02-18 20:20:42 +03:00
} ) ;
2022-04-06 11:40:16 +03:00
await assertMemberEvents ( {
2022-02-18 20:20:42 +03:00
eventType : 'MemberSubscribeEvent' ,
2022-04-06 11:40:16 +03:00
memberId : newMember . id ,
asserts : [
{
subscribed : true ,
2022-04-26 14:21:31 +03:00
source : 'admin' ,
newsletter _id : newsletters [ 0 ] . id
2022-04-06 11:40:16 +03:00
} , {
subscribed : false ,
2022-04-26 14:21:31 +03:00
source : 'admin' ,
newsletter _id : newsletters [ 0 ] . id
2022-04-06 11:40:16 +03:00
}
]
2022-02-18 20:20:42 +03:00
} ) ;
2022-02-14 16:13:54 +03:00
} ) ;
2022-05-04 14:51:35 +03:00
// Internally a different error is thrown for newsletters/products changes
it ( 'Cannot edit a non-existing id with newsletters' , async function ( ) {
const memberChanged = {
name : 'changed' ,
email : 'just-a-member@test.com' ,
newsletters : [ ]
} ;
await agent
. put ( ` /members/ ${ ObjectId ( ) . toHexString ( ) } / ` )
. body ( { members : [ memberChanged ] } )
. expectStatus ( 404 )
. matchBodySnapshot ( {
errors : [ {
id : anyUuid ,
context : anyString
} ]
} )
. matchHeaderSnapshot ( {
etag : anyEtag
} ) ;
} ) ;
it ( 'Cannot edit a non-existing id' , async function ( ) {
const memberChanged = {
name : 'changed' ,
email : 'just-a-member@test.com'
} ;
await agent
. put ( ` /members/ ${ ObjectId ( ) . toHexString ( ) } / ` )
. body ( { members : [ memberChanged ] } )
. expectStatus ( 404 )
. matchBodySnapshot ( {
errors : [ {
id : anyUuid
} ]
} )
. matchHeaderSnapshot ( {
etag : anyEtag
} ) ;
} ) ;
2022-04-26 14:21:31 +03:00
it ( 'Can subscribe to a newsletter' , async function ( ) {
2022-04-27 17:44:27 +03:00
const clock = sinon . useFakeTimers ( Date . now ( ) ) ;
2022-04-26 14:21:31 +03:00
const memberToChange = {
name : 'change me' ,
email : 'member3change@test.com' ,
newsletters : [
newsletters [ 0 ]
]
} ;
const memberChanged = {
newsletters : [
newsletters [ 1 ]
]
} ;
const { body } = await agent
. post ( ` /members/ ` )
. body ( { members : [ memberToChange ] } )
. expectStatus ( 201 )
. matchBodySnapshot ( {
members : new Array ( 1 ) . fill ( memberMatcherShallowIncludes )
} )
. matchHeaderSnapshot ( {
etag : anyEtag ,
location : anyLocationFor ( 'members' )
} ) ;
const newMember = body . members [ 0 ] ;
2022-04-27 17:44:27 +03:00
const before = new Date ( ) ;
before . setMilliseconds ( 0 ) ;
2022-04-26 14:21:31 +03:00
await assertMemberEvents ( {
eventType : 'MemberSubscribeEvent' ,
memberId : newMember . id ,
asserts : [ {
subscribed : true ,
source : 'admin' ,
2022-04-27 17:44:27 +03:00
newsletter _id : newsletters [ 0 ] . id ,
created _at : before
2022-04-26 14:21:31 +03:00
} ]
} ) ;
2022-04-27 17:44:27 +03:00
// Wait 5 second sto guarantee event ordering
clock . tick ( 5000 ) ;
const after = new Date ( ) ;
after . setMilliseconds ( 0 ) ;
2022-05-11 19:56:03 +03:00
2022-04-26 14:21:31 +03:00
await agent
. put ( ` /members/ ${ newMember . id } / ` )
. body ( { members : [ memberChanged ] } )
. expectStatus ( 200 )
. matchBodySnapshot ( {
members : new Array ( 1 ) . fill ( memberMatcherShallowIncludes )
} )
. matchHeaderSnapshot ( {
etag : anyEtag
} ) ;
await assertMemberEvents ( {
eventType : 'MemberSubscribeEvent' ,
memberId : newMember . id ,
asserts : [
{
subscribed : true ,
source : 'admin' ,
2022-04-27 17:44:27 +03:00
newsletter _id : newsletters [ 0 ] . id ,
created _at : before
2022-04-26 14:21:31 +03:00
} , {
subscribed : true ,
source : 'admin' ,
2022-04-27 17:44:27 +03:00
newsletter _id : newsletters [ 1 ] . id ,
created _at : after
2022-04-26 14:21:31 +03:00
} , {
subscribed : false ,
source : 'admin' ,
2022-04-27 17:44:27 +03:00
newsletter _id : newsletters [ 0 ] . id ,
created _at : after
2022-04-26 14:21:31 +03:00
}
]
} ) ;
2022-04-27 17:44:27 +03:00
clock . tick ( 5000 ) ;
// Check activity feed
const { body : eventsBody } = await agent
. get ( ` /members/events?filter=data.member_id: ${ newMember . id } ` )
. body ( { members : [ memberChanged ] } )
. expectStatus ( 200 )
. matchHeaderSnapshot ( {
etag : anyEtag
} ) ;
const events = eventsBody . events ;
events . should . match ( [
{
type : 'newsletter_event' ,
data : {
subscribed : true ,
source : 'admin' ,
newsletter _id : newsletters [ 1 ] . id ,
newsletter : {
id : newsletters [ 1 ] . id
}
}
} ,
{
type : 'newsletter_event' ,
data : {
subscribed : false ,
source : 'admin' ,
newsletter _id : newsletters [ 0 ] . id ,
newsletter : {
id : newsletters [ 0 ] . id
}
}
} ,
{
type : 'newsletter_event' ,
data : {
subscribed : true ,
source : 'admin' ,
newsletter _id : newsletters [ 0 ] . id ,
newsletter : {
id : newsletters [ 0 ] . id
}
}
} ,
{
type : 'signup_event'
}
] ) ;
clock . restore ( ) ;
2022-04-26 14:21:31 +03:00
} ) ;
it ( 'Subscribes to default newsletters' , async function ( ) {
const filtered = newsletters . filter ( n => n . get ( 'subscribe_on_signup' ) ) ;
filtered . length . should . be . greaterThan ( 0 , 'There should be at least one newsletter with subscribe on signup for this test to work' ) ;
const memberToCreate = {
name : 'create me' ,
email : 'member2create@test.com'
} ;
const { body } = await agent
. post ( ` /members/ ` )
. body ( { members : [ memberToCreate ] } )
. expectStatus ( 201 )
. matchBodySnapshot ( {
members : new Array ( 1 ) . fill ( memberMatcherShallowIncludes )
} )
. matchHeaderSnapshot ( {
etag : anyEtag ,
location : anyLocationFor ( 'members' )
} ) ;
const newMember = body . members [ 0 ] ;
newMember . newsletters . should . match ( [
{
id : filtered [ 0 ] . id
} ,
{
id : filtered [ 1 ] . id
}
] ) ;
await assertMemberEvents ( {
eventType : 'MemberSubscribeEvent' ,
memberId : newMember . id ,
asserts : filtered . map ( ( n ) => {
return {
subscribed : true ,
source : 'admin' ,
newsletter _id : n . id
} ;
} )
} ) ;
} ) ;
2022-02-18 07:26:07 +03:00
it ( 'Can add a subscription' , async function ( ) {
2022-02-16 16:23:59 +03:00
const memberId = testUtils . DataGenerator . Content . members [ 0 ] . id ;
const price = testUtils . DataGenerator . Content . stripe _prices [ 0 ] ;
function nockCallback ( method , uri , body ) {
const [ match , resource , id ] = uri . match ( /\/?v1\/(\w+)(?:\/(\w+))?/ ) || [ null ] ;
if ( ! match ) {
return [ 500 ] ;
}
if ( resource === 'customers' ) {
return [ 200 , { id : 'cus_123' , email : 'member1@test.com' } ] ;
}
if ( resource === 'subscriptions' ) {
const now = Math . floor ( Date . now ( ) / 1000 ) ;
return [ 200 , { id : 'sub_123' , customer : 'cus_123' , cancel _at _period _end : false , items : {
data : [ { price : {
id : price . stripe _price _id ,
recurring : {
interval : price . interval
} ,
unit _amount : price . amount ,
currency : price . currency . toLowerCase ( )
} } ]
} , status : 'active' , current _period _end : now + 24 * 3600 , start _date : now } ] ;
}
}
nock ( 'https://api.stripe.com:443' )
. persist ( )
. post ( /v1\/.*/ )
. reply ( ( uri , body ) => nockCallback ( 'POST' , uri , body ) ) ;
nock ( 'https://api.stripe.com:443' )
. persist ( )
. get ( /v1\/.*/ )
. reply ( ( uri , body ) => nockCallback ( 'GET' , uri , body ) ) ;
await agent
. post ( ` /members/ ${ memberId } /subscriptions/ ` )
. body ( {
stripe _price _id : price . id
} )
. expectStatus ( 200 )
. matchBodySnapshot ( {
members : new Array ( 1 ) . fill ( {
id : anyObjectId ,
uuid : anyUuid ,
2022-03-01 14:17:13 +03:00
created _at : anyISODateTime ,
updated _at : anyISODateTime ,
2022-02-16 16:23:59 +03:00
labels : anyArray ,
subscriptions : [ {
start _date : anyString ,
current _period _end : anyString ,
price : {
price _id : anyObjectId ,
product : {
product _id : anyObjectId
}
}
2022-04-26 14:21:31 +03:00
} ] ,
newsletters : anyArray
2022-02-16 16:23:59 +03:00
} )
} )
. matchHeaderSnapshot ( {
etag : anyEtag
} ) ;
2022-03-10 12:10:50 +03:00
// Check member read with a subscription
await agent
. get ( ` /members/ ${ memberId } / ` )
. expectStatus ( 200 )
. matchBodySnapshot ( {
members : new Array ( 1 ) . fill ( {
id : anyObjectId ,
uuid : anyUuid ,
created _at : anyISODateTime ,
updated _at : anyISODateTime ,
labels : anyArray ,
subscriptions : [ {
start _date : anyString ,
current _period _end : anyString ,
price : {
price _id : anyObjectId ,
product : {
product _id : anyObjectId
}
}
2022-04-26 14:21:31 +03:00
} ] ,
newsletters : anyArray
2022-03-10 12:10:50 +03:00
} )
} )
. matchHeaderSnapshot ( {
etag : anyEtag
} ) ;
2022-02-16 16:23:59 +03:00
} ) ;
2022-02-16 21:33:46 +03:00
2022-02-17 17:17:48 +03:00
// Delete a member
2022-02-16 21:33:46 +03:00
2022-02-17 17:17:48 +03:00
it ( 'Can destroy' , async function ( ) {
const member = {
name : 'test' ,
email : 'memberTestDestroy@test.com'
2022-02-16 21:33:46 +03:00
} ;
const { body } = await agent
2022-02-17 17:17:48 +03:00
. post ( ` /members/ ` )
2022-02-16 21:33:46 +03:00
. body ( { members : [ member ] } )
. expectStatus ( 201 )
. matchBodySnapshot ( {
2022-03-18 22:40:11 +03:00
members : new Array ( 1 ) . fill ( memberMatcherShallowIncludes )
2022-02-16 21:33:46 +03:00
} )
. matchHeaderSnapshot ( {
etag : anyEtag ,
2022-02-17 17:17:48 +03:00
location : anyLocationFor ( 'members' )
2022-02-16 21:33:46 +03:00
} ) ;
2022-02-17 17:17:48 +03:00
const newMember = body . members [ 0 ] ;
2022-02-16 21:33:46 +03:00
2022-04-04 15:45:30 +03:00
await agent
. delete ( ` /members/ ${ newMember . id } ` )
. expectStatus ( 204 )
. expectEmptyBody ( )
. matchHeaderSnapshot ( {
etag : anyEtag
} ) ;
await agent
. get ( ` /members/ ${ newMember . id } / ` )
. expectStatus ( 404 )
. matchBodySnapshot ( {
errors : [ {
id : anyUuid
} ]
} )
. matchHeaderSnapshot ( {
etag : anyEtag
} ) ;
} ) ;
it ( 'Can delete a member without cancelling Stripe Subscription' , async function ( ) {
let subscriptionCanceled = false ;
nock ( 'https://api.stripe.com' )
. persist ( )
. delete ( /v1\/.*/ )
. reply ( ( uri ) => {
const [ match , resource , id ] = uri . match ( /\/?v1\/(\w+)(?:\/(\w+))/ ) || [ null ] ;
if ( match && resource === 'subscriptions' ) {
subscriptionCanceled = true ;
return [ 200 , {
id ,
status : 'canceled'
} ] ;
}
return [ 500 ] ;
} ) ;
// @TODO This is wrong because it changes the state for the rest of the tests
// We need to add a member via a fixture and then remove them OR work out how
// to reapply fixtures before each test
const memberToDelete = fixtureManager . get ( 'members' , 2 ) ;
await agent
. delete ( ` members/ ${ memberToDelete . id } / ` )
. expectStatus ( 204 )
. expectEmptyBody ( )
. matchHeaderSnapshot ( {
etag : anyEtag
} ) ;
assert . equal ( subscriptionCanceled , false , 'expected subscription not to be canceled' ) ;
} ) ;
// Export members to CSV
it ( 'Can export CSV' , async function ( ) {
const res = await agent
2022-05-09 11:56:36 +03:00
. get ( ` /members/upload/?limit=all ` )
2022-04-04 15:45:30 +03:00
. expectStatus ( 200 )
. expectEmptyBody ( ) // express-test body parsing doesn't support CSV
. matchHeaderSnapshot ( {
etag : anyEtag ,
'content-length' : anyString , //For some reason the content-length changes between 1220 and 1317
'content-disposition' : anyString
} ) ;
res . text . should . match ( /id,email,name,note,subscribed_to_emails,complimentary_plan,stripe_customer_id,created_at,deleted_at/ ) ;
const csv = Papa . parse ( res . text , { header : true } ) ;
should . exist ( csv . data . find ( row => row . name === 'Mr Egg' ) ) ;
should . exist ( csv . data . find ( row => row . name === 'Winston Zeddemore' ) ) ;
should . exist ( csv . data . find ( row => row . name === 'Ray Stantz' ) ) ;
should . exist ( csv . data . find ( row => row . email === 'member2@test.com' ) ) ;
} ) ;
it ( 'Can export a filtered CSV' , async function ( ) {
const res = await agent
. get ( ` /members/upload/?search=Egg ` )
. expectStatus ( 200 )
. expectEmptyBody ( ) // express-test body parsing doesn't support CSV
. matchHeaderSnapshot ( {
etag : anyEtag ,
'content-disposition' : anyString
} ) ;
res . text . should . match ( /id,email,name,note,subscribed_to_emails,complimentary_plan,stripe_customer_id,created_at,deleted_at/ ) ;
const csv = Papa . parse ( res . text , { header : true } ) ;
should . exist ( csv . data . find ( row => row . name === 'Mr Egg' ) ) ;
should . not . exist ( csv . data . find ( row => row . name === 'Egon Spengler' ) ) ;
should . not . exist ( csv . data . find ( row => row . name === 'Ray Stantz' ) ) ;
should . not . exist ( csv . data . find ( row => row . email === 'member2@test.com' ) ) ;
} ) ;
// Get stats
it ( 'Can fetch member counts stats' , async function ( ) {
await agent
. get ( ` /members/stats/count/ ` )
. expectStatus ( 200 )
. matchBodySnapshot ( {
data : [ {
date : anyISODate
} ]
} )
. matchHeaderSnapshot ( {
etag : anyEtag
} ) ;
} ) ;
it ( 'Errors when fetching stats with unknown days param value' , async function ( ) {
await agent
. get ( 'members/stats/?days=nope' )
. expectStatus ( 422 )
. matchHeaderSnapshot ( {
etag : anyEtag
} )
. matchBodySnapshot ( {
errors : [ {
id : anyErrorId
} ]
} ) ;
} ) ;
2022-04-12 18:31:49 +03:00
it ( 'Can filter on newsletter slug' , async function ( ) {
await agent
2022-04-18 11:18:44 +03:00
. get ( '/members/?filter=newsletters:weekly-newsletter' )
2022-04-12 18:31:49 +03:00
. expectStatus ( 200 )
. matchBodySnapshot ( {
2022-04-26 14:21:31 +03:00
members : new Array ( 4 ) . fill ( memberMatcherShallowIncludes )
2022-04-12 18:31:49 +03:00
} )
. matchHeaderSnapshot ( {
etag : anyEtag
} ) ;
} ) ;
2022-04-07 06:00:00 +03:00
// Edit a member
2022-04-12 17:14:21 +03:00
it ( 'Can add and edit with custom newsletters' , async function ( ) {
2022-04-07 06:00:00 +03:00
// Add custom newsletter list to new member
2022-04-04 15:45:30 +03:00
const member = {
2022-04-07 06:00:00 +03:00
name : 'test newsletter' ,
email : 'memberTestAddNewsletter2@test.com' ,
note : 'test note' ,
subscribed : false ,
labels : [ 'test-label' ] ,
newsletters : [ { id : testUtils . DataGenerator . Content . newsletters [ 1 ] . id } ]
2022-04-04 15:45:30 +03:00
} ;
const { body } = await agent
2022-04-07 06:00:00 +03:00
. post ( ` /members/ ` )
2022-04-04 15:45:30 +03:00
. body ( { members : [ member ] } )
. expectStatus ( 201 )
. matchBodySnapshot ( {
2022-04-07 06:00:00 +03:00
members : [ {
id : anyObjectId ,
uuid : anyUuid ,
created _at : anyISODateTime ,
updated _at : anyISODateTime ,
subscriptions : anyArray ,
labels : anyArray ,
2022-04-22 15:39:27 +03:00
newsletters : Array ( 1 ) . fill ( newsletterSnapshot )
2022-04-07 06:00:00 +03:00
} ]
2022-04-04 15:45:30 +03:00
} )
. matchHeaderSnapshot ( {
etag : anyEtag ,
2022-04-07 06:00:00 +03:00
location : anyLocationFor ( 'members' )
2022-04-04 15:45:30 +03:00
} ) ;
2022-04-07 06:00:00 +03:00
const memberId = body . members [ 0 ] . id ;
const editedMember = {
newsletters : [ { id : testUtils . DataGenerator . Content . newsletters [ 0 ] . id } ]
} ;
2022-04-04 15:45:30 +03:00
2022-04-07 06:00:00 +03:00
// Edit newsletter list for member
2022-04-04 15:45:30 +03:00
await agent
2022-04-07 06:00:00 +03:00
. put ( ` /members/ ${ memberId } ` )
. body ( { members : [ editedMember ] } )
. expectStatus ( 200 )
. matchBodySnapshot ( {
members : [ {
id : anyObjectId ,
uuid : anyUuid ,
created _at : anyISODateTime ,
updated _at : anyISODateTime ,
subscriptions : anyArray ,
labels : anyArray ,
2022-04-22 15:39:27 +03:00
newsletters : Array ( 1 ) . fill ( newsletterSnapshot )
2022-04-07 06:00:00 +03:00
} ]
} )
2022-04-04 15:45:30 +03:00
. matchHeaderSnapshot ( {
etag : anyEtag
2022-04-07 06:00:00 +03:00
} ) ;
2022-04-04 15:45:30 +03:00
await agent
. post ( ` /members/ ` )
. body ( { members : [ member ] } )
2022-04-07 06:00:00 +03:00
. expectStatus ( 422 ) ;
2022-02-16 21:33:46 +03:00
} ) ;
2022-04-27 18:04:55 +03:00
it ( 'Can add and send a signup confirmation email (old)' , async function ( ) {
const filteredNewsletters = newsletters . filter ( n => n . get ( 'subscribe_on_signup' ) ) ;
filteredNewsletters . length . should . be . greaterThan ( 0 , 'For this test to work, we need at least one newsletter fixture with subscribe_on_signup = true' ) ;
const member = {
name : 'Send Me Confirmation' ,
email : 'member_getting_confirmation_old@test.com' ,
// Mapped to subscribe_on_signup newsletters
subscribed : true
} ;
const { body } = await agent
. post ( '/members/?send_email=true&email_type=signup' )
. body ( { members : [ member ] } )
. expectStatus ( 201 )
. matchBodySnapshot ( {
members : [
buildMemberWithoutIncludesSnapshot ( {
newsletters : filteredNewsletters . length
} )
]
} )
. matchHeaderSnapshot ( {
etag : anyEtag ,
location : anyString
} ) ;
const newMember = body . members [ 0 ] ;
mockManager . assert . sentEmail ( {
subject : '🙌 Complete your sign up to Ghost!' ,
to : 'member_getting_confirmation_old@test.com'
} ) ;
await assertMemberEvents ( {
eventType : 'MemberStatusEvent' ,
memberId : newMember . id ,
asserts : [
{
from _status : null ,
to _status : 'free'
}
]
} ) ;
await assertMemberEvents ( {
eventType : 'MemberSubscribeEvent' ,
memberId : newMember . id ,
asserts : filteredNewsletters . map ( ( n ) => {
return {
subscribed : true ,
newsletter _id : n . id ,
source : 'admin'
} ;
} )
} ) ;
// @TODO: do we really need to delete this member here?
await agent
. delete ( ` members/ ${ body . members [ 0 ] . id } / ` )
. matchHeaderSnapshot ( {
etag : anyEtag
} )
. expectStatus ( 204 ) ;
// There should be no MemberSubscribeEvent remaining.
await assertMemberEvents ( {
eventType : 'MemberSubscribeEvent' ,
memberId : newMember . id ,
asserts : [ ]
} ) ;
} ) ;
it ( 'Can add a member that is not subscribed (old)' , async function ( ) {
const filteredNewsletters = newsletters . filter ( n => n . get ( 'subscribe_on_signup' ) ) ;
filteredNewsletters . length . should . be . greaterThan ( 0 , 'For this test to work, we need at least one newsletter fixture with subscribe_on_signup = true' ) ;
const member = {
name : 'Send Me Confirmation' ,
email : 'member_getting_confirmation_old_2@test.com' ,
// Mapped to empty newsletters
subscribed : false
} ;
const { body } = await agent
. post ( '/members/?send_email=true&email_type=signup' )
. body ( { members : [ member ] } )
. expectStatus ( 201 )
. matchBodySnapshot ( {
members : [
buildMemberWithoutIncludesSnapshot ( {
newsletters : 0
} )
]
} )
. matchHeaderSnapshot ( {
etag : anyEtag ,
location : anyString
} ) ;
const newMember = body . members [ 0 ] ;
mockManager . assert . sentEmail ( {
subject : '🙌 Complete your sign up to Ghost!' ,
to : 'member_getting_confirmation_old_2@test.com'
} ) ;
await assertMemberEvents ( {
eventType : 'MemberStatusEvent' ,
memberId : newMember . id ,
asserts : [
{
from _status : null ,
to _status : 'free'
}
]
} ) ;
await assertMemberEvents ( {
eventType : 'MemberSubscribeEvent' ,
memberId : newMember . id ,
asserts : [ ]
} ) ;
} ) ;
it ( 'Can unsubscribe by setting (old) subscribed property to false' , async function ( ) {
const memberToChange = {
name : 'change me' ,
email : 'member2unsusbcribeold@test.com' ,
note : 'initial note' ,
newsletters : [
newsletters [ 0 ]
]
} ;
const memberChanged = {
subscribed : false
} ;
const { body } = await agent
. post ( ` /members/ ` )
. body ( { members : [ memberToChange ] } )
. expectStatus ( 201 )
. matchBodySnapshot ( {
members : [
buildMemberWithIncludesSnapshot ( {
newsletters : 1
} )
]
} )
. matchHeaderSnapshot ( {
etag : anyEtag ,
location : anyLocationFor ( 'members' )
} ) ;
const newMember = body . members [ 0 ] ;
await assertMemberEvents ( {
eventType : 'MemberSubscribeEvent' ,
memberId : newMember . id ,
asserts : [ {
subscribed : true ,
source : 'admin' ,
newsletter _id : newsletters [ 0 ] . id
} ]
} ) ;
await assertMemberEvents ( {
eventType : 'MemberStatusEvent' ,
memberId : newMember . id ,
asserts : [ {
from _status : null ,
to _status : 'free'
} ]
} ) ;
await agent
. put ( ` /members/ ${ newMember . id } / ` )
. body ( { members : [ memberChanged ] } )
. expectStatus ( 200 )
. matchBodySnapshot ( {
members : [
buildMemberWithIncludesSnapshot ( {
newsletters : 0
} )
]
} )
. matchHeaderSnapshot ( {
etag : anyEtag
} ) ;
await assertMemberEvents ( {
eventType : 'MemberSubscribeEvent' ,
memberId : newMember . id ,
asserts : [
{
subscribed : true ,
source : 'admin' ,
newsletter _id : newsletters [ 0 ] . id
} , {
subscribed : false ,
source : 'admin' ,
newsletter _id : newsletters [ 0 ] . id
}
]
} ) ;
} ) ;
it ( 'Can subscribe by setting (old) subscribed property to true' , async function ( ) {
const filteredNewsletters = newsletters . filter ( n => n . get ( 'subscribe_on_signup' ) ) ;
filteredNewsletters . length . should . be . greaterThan ( 0 , 'For this test to work, we need at least one newsletter fixture with subscribe_on_signup = true' ) ;
const memberToChange = {
name : 'change me' ,
email : 'member2subscribe@test.com' ,
note : 'initial note' ,
newsletters : [ ]
} ;
const memberChanged = {
subscribed : true
} ;
const { body } = await agent
. post ( ` /members/ ` )
. body ( { members : [ memberToChange ] } )
. expectStatus ( 201 )
. matchBodySnapshot ( {
members : [
buildMemberWithIncludesSnapshot ( {
newsletters : 0
} )
]
} )
. matchHeaderSnapshot ( {
etag : anyEtag ,
location : anyLocationFor ( 'members' )
} ) ;
const newMember = body . members [ 0 ] ;
await assertMemberEvents ( {
eventType : 'MemberSubscribeEvent' ,
memberId : newMember . id ,
asserts : [ ]
} ) ;
await assertMemberEvents ( {
eventType : 'MemberStatusEvent' ,
memberId : newMember . id ,
asserts : [ {
from _status : null ,
to _status : 'free'
} ]
} ) ;
await agent
. put ( ` /members/ ${ newMember . id } / ` )
. body ( { members : [ memberChanged ] } )
. expectStatus ( 200 )
. matchBodySnapshot ( {
members : [
buildMemberWithIncludesSnapshot ( {
newsletters : filteredNewsletters . length
} )
]
} )
. matchHeaderSnapshot ( {
etag : anyEtag
} ) ;
await assertMemberEvents ( {
eventType : 'MemberSubscribeEvent' ,
memberId : newMember . id ,
asserts : filteredNewsletters . map ( ( n ) => {
return {
subscribed : true ,
source : 'admin' ,
newsletter _id : n . id
} ;
} )
} ) ;
} ) ;
2022-02-14 16:13:54 +03:00
} ) ;