2022-02-14 16:13:54 +03:00
const { agentProvider , mockManager , fixtureManager , matchers } = require ( '../../utils/e2e-framework' ) ;
2022-10-05 12:34:17 +03:00
const { anyEtag , anyObjectId , anyUuid , anyISODateTime , anyISODate , anyString , anyArray , anyLocationFor , anyContentLength , anyErrorId , anyObject } = matchers ;
2022-10-12 10:54:35 +03:00
const ObjectId = require ( 'bson-objectid' ) . default ;
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-08-24 17:11:25 +03:00
const membersService = require ( '../../../core/server/services/members' ) ;
const memberAttributionService = require ( '../../../core/server/services/member-attribution' ) ;
const urlService = require ( '../../../core/server/services/url' ) ;
const urlUtils = require ( '../../../core/shared/url-utils' ) ;
2022-02-18 20:20:42 +03:00
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' } ) ;
}
2022-08-30 18:36:52 +03:00
async function getOtherPaidProduct ( ) {
return ( await models . Product . findAll ( { type : 'paid' } ) ) . models [ 0 ] ;
}
2022-04-26 14:21:31 +03:00
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-05-17 17:38:25 +03:00
const subscriptionSnapshot = {
start _date : anyString ,
current _period _end : anyString ,
price : {
price _id : anyObjectId ,
tier : {
tier _id : anyObjectId
}
}
} ;
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-07-15 13:16:06 +03:00
const tierMatcher = {
id : anyObjectId ,
created _at : anyISODateTime ,
updated _at : anyISODateTime ,
monthly _price _id : anyObjectId ,
yearly _price _id : anyObjectId
} ;
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-07-15 13:16:06 +03:00
const buildMemberMatcherShallowIncludesWithTiers = ( tiersCount ) => {
let tiers = anyArray ;
if ( tiersCount ) {
tiers = new Array ( tiers ) . fill ( tierMatcher ) ;
}
return {
... memberMatcherShallowIncludes ,
tiers
} ;
2022-05-11 19:56:03 +03:00
} ;
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-08-24 17:11:25 +03:00
// Tests specific for member attribution
describe ( 'Members API - member attribution' , function ( ) {
const signupAttributions = [ ] ;
before ( async function ( ) {
agent = await agentProvider . getAdminAPIAgent ( ) ;
await fixtureManager . init ( 'posts' , 'newsletters' , 'members:newsletters' , 'comments' ) ;
await agent . loginAsOwner ( ) ;
// This is required so that the only members in this test are created by this test, and not from fixtures.
await models . Member . query ( ) . del ( ) ;
} ) ;
beforeEach ( function ( ) {
mockManager . mockStripe ( ) ;
mockManager . mockMail ( ) ;
// For some reason it is enabled by default?
mockManager . mockLabsEnabled ( 'memberAttribution' ) ;
} ) ;
afterEach ( function ( ) {
mockManager . restore ( ) ;
} ) ;
it ( 'Can read member attributed to a post' , async function ( ) {
const id = fixtureManager . get ( 'posts' , 0 ) . id ;
const post = await models . Post . where ( 'id' , id ) . fetch ( { require : true } ) ;
// Set the attribution for this member manually
const member = await membersService . api . members . create ( {
email : 'member-attributed-to-post@test.com' ,
attribution : memberAttributionService . attributionBuilder . build ( {
id ,
url : '/out-of-date/' ,
2022-09-23 18:19:51 +03:00
type : 'post' ,
2022-09-27 22:28:06 +03:00
referrerSource : null ,
referrerMedium : null ,
referrerUrl : null
2022-08-24 17:11:25 +03:00
} )
} ) ;
const absoluteUrl = urlService . getUrlByResourceId ( post . id , { absolute : true } ) ;
await agent
. get ( ` /members/ ${ member . id } / ` )
. expectStatus ( 200 )
. matchBodySnapshot ( {
members : new Array ( 1 ) . fill ( memberMatcherShallowIncludes )
} )
. matchHeaderSnapshot ( {
etag : anyEtag
} )
. expect ( ( { body } ) => {
should ( body . members [ 0 ] . attribution ) . eql ( {
id : post . id ,
url : absoluteUrl ,
type : 'post' ,
2022-09-23 18:19:51 +03:00
title : post . get ( 'title' ) ,
referrer _source : null ,
referrer _medium : null ,
referrer _url : null
2022-08-24 17:11:25 +03:00
} ) ;
signupAttributions . push ( body . members [ 0 ] . attribution ) ;
} ) ;
} ) ;
it ( 'Can read member attributed to a page' , async function ( ) {
const id = fixtureManager . get ( 'posts' , 5 ) . id ;
const post = await models . Post . where ( 'id' , id ) . fetch ( { require : true } ) ;
// Set the attribution for this member manually
const member = await membersService . api . members . create ( {
email : 'member-attributed-to-page@test.com' ,
attribution : memberAttributionService . attributionBuilder . build ( {
id ,
url : '/out-of-date/' ,
2022-09-23 18:19:51 +03:00
type : 'page' ,
2022-09-27 22:28:06 +03:00
referrerSource : null ,
referrerMedium : null ,
referrerUrl : null
2022-08-24 17:11:25 +03:00
} )
} ) ;
const absoluteUrl = urlService . getUrlByResourceId ( post . id , { absolute : true } ) ;
await agent
. get ( ` /members/ ${ member . id } / ` )
. expectStatus ( 200 )
. matchBodySnapshot ( {
members : new Array ( 1 ) . fill ( memberMatcherShallowIncludes )
} )
. matchHeaderSnapshot ( {
etag : anyEtag
} )
. expect ( ( { body } ) => {
should ( body . members [ 0 ] . attribution ) . eql ( {
id : post . id ,
url : absoluteUrl ,
type : 'page' ,
2022-09-23 18:19:51 +03:00
title : post . get ( 'title' ) ,
referrer _source : null ,
referrer _medium : null ,
referrer _url : null
2022-08-24 17:11:25 +03:00
} ) ;
signupAttributions . push ( body . members [ 0 ] . attribution ) ;
} ) ;
} ) ;
it ( 'Can read member attributed to a tag' , async function ( ) {
const id = fixtureManager . get ( 'tags' , 0 ) . id ;
const tag = await models . Tag . where ( 'id' , id ) . fetch ( { require : true } ) ;
// Set the attribution for this member manually
const member = await membersService . api . members . create ( {
email : 'member-attributed-to-tag@test.com' ,
attribution : memberAttributionService . attributionBuilder . build ( {
id ,
url : '/out-of-date/' ,
2022-09-23 18:19:51 +03:00
type : 'tag' ,
2022-09-27 22:28:06 +03:00
referrerSource : null ,
referrerMedium : null ,
referrerUrl : null
2022-08-24 17:11:25 +03:00
} )
} ) ;
const absoluteUrl = urlService . getUrlByResourceId ( tag . id , { absolute : true } ) ;
await agent
. get ( ` /members/ ${ member . id } / ` )
. expectStatus ( 200 )
. matchBodySnapshot ( {
members : new Array ( 1 ) . fill ( memberMatcherShallowIncludes )
} )
. matchHeaderSnapshot ( {
etag : anyEtag
} )
. expect ( ( { body } ) => {
should ( body . members [ 0 ] . attribution ) . eql ( {
id : tag . id ,
url : absoluteUrl ,
type : 'tag' ,
2022-09-23 18:19:51 +03:00
title : tag . get ( 'name' ) ,
referrer _source : null ,
referrer _medium : null ,
referrer _url : null
2022-08-24 17:11:25 +03:00
} ) ;
signupAttributions . push ( body . members [ 0 ] . attribution ) ;
} ) ;
} ) ;
it ( 'Can read member attributed to an author' , async function ( ) {
const id = fixtureManager . get ( 'users' , 0 ) . id ;
const author = await models . User . where ( 'id' , id ) . fetch ( { require : true } ) ;
// Set the attribution for this member manually
const member = await membersService . api . members . create ( {
email : 'member-attributed-to-author@test.com' ,
attribution : memberAttributionService . attributionBuilder . build ( {
id ,
url : '/out-of-date/' ,
2022-09-23 18:19:51 +03:00
type : 'author' ,
2022-09-27 22:28:06 +03:00
referrerSource : null ,
referrerMedium : null ,
referrerUrl : null
2022-08-24 17:11:25 +03:00
} )
} ) ;
const absoluteUrl = urlService . getUrlByResourceId ( author . id , { absolute : true } ) ;
await agent
. get ( ` /members/ ${ member . id } / ` )
. expectStatus ( 200 )
. matchBodySnapshot ( {
members : new Array ( 1 ) . fill ( memberMatcherShallowIncludes )
} )
. matchHeaderSnapshot ( {
etag : anyEtag
} )
. expect ( ( { body } ) => {
should ( body . members [ 0 ] . attribution ) . eql ( {
id : author . id ,
url : absoluteUrl ,
type : 'author' ,
2022-09-23 18:19:51 +03:00
title : author . get ( 'name' ) ,
referrer _source : null ,
referrer _medium : null ,
referrer _url : null
2022-08-24 17:11:25 +03:00
} ) ;
signupAttributions . push ( body . members [ 0 ] . attribution ) ;
} ) ;
} ) ;
it ( 'Can read member attributed to an url' , async function ( ) {
// Set the attribution for this member manually
const member = await membersService . api . members . create ( {
email : 'member-attributed-to-url@test.com' ,
attribution : memberAttributionService . attributionBuilder . build ( {
id : null ,
url : '/a-static-page/' ,
2022-09-23 18:19:51 +03:00
type : 'url' ,
2022-09-27 22:28:06 +03:00
referrerSource : null ,
referrerMedium : null ,
referrerUrl : null
2022-08-24 17:11:25 +03:00
} )
} ) ;
const absoluteUrl = urlUtils . createUrl ( '/a-static-page/' , true ) ;
await agent
. get ( ` /members/ ${ member . id } / ` )
. expectStatus ( 200 )
. matchBodySnapshot ( {
members : new Array ( 1 ) . fill ( memberMatcherShallowIncludes )
} )
. matchHeaderSnapshot ( {
etag : anyEtag
} )
. expect ( ( { body } ) => {
should ( body . members [ 0 ] . attribution ) . eql ( {
id : null ,
url : absoluteUrl ,
type : 'url' ,
2022-09-23 18:19:51 +03:00
title : '/a-static-page/' ,
referrer _source : null ,
referrer _medium : null ,
referrer _url : null
2022-08-24 17:11:25 +03:00
} ) ;
signupAttributions . push ( body . members [ 0 ] . attribution ) ;
} ) ;
} ) ;
// Activity feed
2022-10-18 16:52:04 +03:00
it ( 'Returns sign up attributions of all types in activity feed' , async function ( ) {
2022-08-24 17:11:25 +03:00
// Check activity feed
await agent
. get ( ` /members/events/?filter=type:signup_event ` )
. expectStatus ( 200 )
. matchHeaderSnapshot ( {
etag : anyEtag
} )
. matchBodySnapshot ( {
events : new Array ( signupAttributions . length ) . fill ( {
type : anyString ,
data : anyObject
} )
} )
. expect ( ( { body } ) => {
should ( body . events . find ( e => e . type !== 'signup_event' ) ) . be . undefined ( ) ;
should ( body . events . map ( e => e . data . attribution ) ) . containDeep ( signupAttributions ) ;
} ) ;
} ) ;
} ) ;
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-10-06 12:43:39 +03:00
await fixtureManager . init ( 'posts' , 'newsletters' , 'members:newsletters' , 'comments' , 'redirects' , 'clicks' ) ;
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
} ) ;
} ) ;
2022-08-22 21:19:13 +03:00
it ( 'Can filter by signup attribution' , async function ( ) {
await agent
. get ( '/members/?filter=signup:' + fixtureManager . get ( 'posts' , 0 ) . id )
. expectStatus ( 200 )
. matchBodySnapshot ( {
members : new Array ( 3 ) . fill ( memberMatcherShallowIncludes )
} )
. matchHeaderSnapshot ( {
etag : anyEtag
} ) ;
} ) ;
2022-08-22 21:23:44 +03:00
it ( 'Can filter by signup attribution' , async function ( ) {
await agent
. get ( '/members/?filter=conversion:' + fixtureManager . get ( 'posts' , 0 ) . id )
. expectStatus ( 200 )
. matchBodySnapshot ( {
members : new Array ( 1 ) . fill ( memberMatcherShallowIncludes )
} )
. matchHeaderSnapshot ( {
etag : anyEtag
} ) ;
} ) ;
2022-08-22 21:19:13 +03:00
2022-02-14 16:13:54 +03:00
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 ,
2022-10-05 12:34:17 +03:00
'content-length' : anyContentLength
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 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 ,
2022-10-05 12:34:17 +03:00
'content-length' : anyContentLength
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 ( {
2022-07-15 13:16:06 +03:00
members : new Array ( 1 ) . fill ( buildMemberMatcherShallowIncludesWithTiers ( ) )
2022-05-11 19:56:03 +03:00
} )
. 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 ] ;
2022-08-30 18:36:52 +03:00
const updatedMember = await agent
2022-02-14 16:13:54 +03:00
. put ( ` /members/ ${ newMember . id } / ` )
. body ( { members : [ compedPayload ] } )
. expectStatus ( 200 )
. matchBodySnapshot ( {
2022-07-15 13:16:06 +03:00
members : new Array ( 1 ) . fill ( buildMemberMatcherShallowIncludesWithTiers ( 1 ) )
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-07-15 13:16:06 +03:00
} , {
from _status : 'free' ,
to _status : 'comped'
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' ) ;
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' ) ;
2022-05-17 17:38:25 +03:00
assert . equal ( updatedMember . tiers . length , 0 , 'The member should have 0 tiers' ) ;
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 : [ ]
} ) ;
} ) ;
2022-09-02 11:12:44 +03:00
it ( 'Can create a new member with a product (complimentary)' , async function ( ) {
2022-04-06 11:40:16 +03:00
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 ,
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 = {
2022-07-15 13:16:06 +03:00
id : 'sub_2' ,
2022-04-06 11:40:16 +03:00
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-07-15 13:16:06 +03:00
tiers : new Array ( 1 ) . fill ( tierMatcher ) ,
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-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
2022-08-30 18:36:52 +03:00
// Save this member for the next tests
2022-04-21 13:06:06 +03:00
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 ] ;
2022-05-17 17:38:25 +03:00
// Note that we explicitly need to ask to include tiers 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 ] ;
2022-08-25 22:25:01 +03:00
// Ignore attribution for now
delete readMember . attribution ;
for ( const sub of readMember . subscriptions ) {
delete sub . attribution ;
}
// Ignore attribution for now
delete memberWithPaidSubscription . attribution ;
for ( const sub of memberWithPaidSubscription . subscriptions ) {
delete sub . attribution ;
}
2022-04-21 13:06:06 +03:00
// 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
} ) ;
2022-08-30 18:36:52 +03:00
it ( 'Cannot add complimentary subscriptions to a member with an active subscription' , async function ( ) {
if ( ! memberWithPaidSubscription ) {
// Previous test failed
this . skip ( ) ;
}
const product = await getOtherPaidProduct ( ) ;
const compedPayload = {
id : memberWithPaidSubscription . id ,
tiers : [
... memberWithPaidSubscription . tiers ,
{
id : product . id
}
]
} ;
await agent
. put ( ` /members/ ${ memberWithPaidSubscription . id } / ` )
. body ( { members : [ compedPayload ] } )
. expectStatus ( 400 ) ;
} ) ;
it ( 'Cannot remove non complimentary subscriptions directly from a member' , async function ( ) {
if ( ! memberWithPaidSubscription ) {
// Previous test failed
this . skip ( ) ;
}
const compedPayload = {
id : memberWithPaidSubscription . id ,
// Remove all paid subscriptions (= not allowed atm)
tiers : [ ]
} ;
await agent
. put ( ` /members/ ${ memberWithPaidSubscription . id } / ` )
. body ( { members : [ compedPayload ] } )
. expectStatus ( 400 ) ;
} ) ;
it ( 'Can remove a complimentary subscription directly from a member with other active subscriptions' , async function ( ) {
// This tests for an edge case that shouldn't be possible, but the API should support this to resolve issues
// refs https://github.com/TryGhost/Team/issues/1859
if ( ! memberWithPaidSubscription ) {
// Previous test failed
this . skip ( ) ;
}
// Check that the product that we are going to add is not the same as the existing one
const product = await getOtherPaidProduct ( ) ;
should ( memberWithPaidSubscription . tiers ) . have . length ( 1 ) ;
should ( memberWithPaidSubscription . tiers [ 0 ] . id ) . not . eql ( product . id ) ;
// Add it manually
const member = await models . Member . edit ( {
products : [
... memberWithPaidSubscription . tiers ,
{
id : product . id
}
]
} , { id : memberWithPaidSubscription . id } ) ;
// Check status
const { body : body2 } = await agent
. get ( ` /members/ ${ memberWithPaidSubscription . id } / ` )
. expectStatus ( 200 ) ;
const beforeMember = body2 . members [ 0 ] ;
2022-10-21 12:45:02 +03:00
assert . equal ( beforeMember . tiers . length , 2 , 'The member should have two tiers now' ) ;
2022-08-30 18:36:52 +03:00
// Now try to remove only the complimentary one
const compedPayload = {
id : memberWithPaidSubscription . id ,
// Remove all complimentary subscriptions
tiers : memberWithPaidSubscription . tiers
} ;
const { body } = await agent
. put ( ` /members/ ${ memberWithPaidSubscription . id } / ` )
. body ( { members : [ compedPayload ] } )
. expectStatus ( 200 ) ;
const updatedMember = body . members [ 0 ] ;
assert . equal ( updatedMember . status , 'paid' , 'Member should still have the paid status' ) ;
assert . equal ( updatedMember . tiers . length , 1 , 'The member should have one product now' ) ;
assert . equal ( updatedMember . tiers [ 0 ] . id , memberWithPaidSubscription . tiers [ 0 ] . id , 'The member should have the paid product' ) ;
} ) ;
it ( 'Can keep tiers unchanged when modifying a paid member' , async function ( ) {
if ( ! memberWithPaidSubscription ) {
// Previous test failed
this . skip ( ) ;
}
const compedPayload = {
id : memberWithPaidSubscription . id ,
// Not changed tiers
tiers : [ ... memberWithPaidSubscription . tiers ]
} ;
await agent
. put ( ` /members/ ${ memberWithPaidSubscription . id } / ` )
. body ( { members : [ compedPayload ] } )
. expectStatus ( 200 ) ;
} ) ;
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-17 17:38:25 +03:00
// Internally a different error is thrown for newsletters/tiers changes
2022-05-04 14:51:35 +03:00
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
2022-10-11 12:24:11 +03:00
// Wait 5 seconds to guarantee event ordering
2022-04-27 17:44:27 +03:00
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
}
}
} ,
2022-10-18 16:52:04 +03:00
{
type : 'signup_event'
} ,
2022-04-27 17:44:27 +03:00
{
type : 'newsletter_event' ,
data : {
subscribed : true ,
source : 'admin' ,
newsletter _id : newsletters [ 0 ] . id ,
newsletter : {
id : newsletters [ 0 ] . id
}
}
}
] ) ;
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 ,
2022-05-17 17:38:25 +03:00
subscriptions : [ subscriptionSnapshot ] ,
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 ,
2022-05-17 17:38:25 +03:00
subscriptions : [ subscriptionSnapshot ] ,
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-10-11 12:15:27 +03:00
it ( 'Can edit a subscription' , async function ( ) {
const memberId = testUtils . DataGenerator . Content . members [ 1 ] . id ;
const price = testUtils . DataGenerator . Content . stripe _prices [ 0 ] ;
const stripeCustomerId = 'cus_GbEMMOZNVrL450' ;
const stripeSubscriptionId = 'sub_K1cBgJt6sCMu5n' ;
const stripeSubscriptionFixture = ( { status = 'active' } = { } ) => {
const now = Math . floor ( Date . now ( ) / 1000 ) ;
return {
id : stripeSubscriptionId ,
customer : stripeCustomerId ,
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 : status ,
current _period _end : now + 24 * 3600 ,
start _date : now
} ;
} ;
nock ( 'https://api.stripe.com:443' )
. post ( '/v1/customers' )
. reply ( 200 , {
id : ` cus_GbEMMOZNVrL450 ` ,
email : 'member1@test.com'
} ) ;
nock ( 'https://api.stripe.com:443' )
. get ( ` /v1/subscriptions/ ${ stripeSubscriptionId } ` )
. reply ( 200 , stripeSubscriptionFixture ( ) ) ;
nock ( 'https://api.stripe.com:443' )
. post ( '/v1/subscriptions' )
. reply ( 200 , stripeSubscriptionFixture ( ) ) ;
const res = await agent
. post ( ` /members/ ${ memberId } /subscriptions/ ` )
. body ( {
stripe _price _id : price . id
} )
. expectStatus ( 200 )
. matchBodySnapshot ( {
members : new Array ( 1 ) . fill ( {
id : anyObjectId ,
uuid : anyUuid ,
created _at : anyISODateTime ,
updated _at : anyISODateTime ,
labels : anyArray ,
subscriptions : [ subscriptionSnapshot ] ,
newsletters : anyArray
} )
} )
. matchHeaderSnapshot ( {
etag : anyEtag
} ) ;
const subscriptionId = res . body . members [ 0 ] . subscriptions [ 0 ] . id ;
nock ( 'https://api.stripe.com:443' )
. delete ( ` /v1/subscriptions/ ${ stripeSubscriptionId } ` )
. reply ( 200 , stripeSubscriptionFixture ( { status : 'canceled' } ) ) ;
nock ( 'https://api.stripe.com:443' )
. get ( ` /v1/subscriptions/ ${ stripeSubscriptionId } ` )
. reply ( 200 , stripeSubscriptionFixture ( { status : 'canceled' } ) ) ;
const editRes = await agent
. put ( ` /members/ ${ memberId } /subscriptions/ ${ subscriptionId } ` )
. body ( {
status : 'canceled'
} )
. expectStatus ( 200 )
. matchBodySnapshot ( {
members : new Array ( 1 ) . fill ( {
id : anyObjectId ,
uuid : anyUuid ,
created _at : anyISODateTime ,
updated _at : anyISODateTime ,
labels : anyArray ,
subscriptions : [ subscriptionSnapshot ] ,
newsletters : anyArray
} )
} )
. matchHeaderSnapshot ( {
etag : anyEtag
} ) ;
assert . equal ( 'canceled' , editRes . body . members [ 0 ] . subscriptions [ 0 ] . status ) ;
} ) ;
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
} ]
} )
2022-08-24 10:28:20 +03:00
. matchHeaderSnapshot ( {
etag : anyEtag
} ) ;
} ) ;
it ( 'Cannot delete a non-existent member' , async function ( ) {
await agent
. delete ( '/members/abcd1234abcd1234abcd1234' )
. expectStatus ( 404 )
. matchBodySnapshot ( {
errors : [ {
id : anyUuid
} ]
} )
2022-04-04 15:45:30 +03:00
. 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 ,
2022-10-05 12:34:17 +03:00
'content-length' : anyContentLength ,
2022-04-04 15:45:30 +03:00
'content-disposition' : anyString
} ) ;
2022-10-21 12:45:02 +03:00
res . text . should . match ( /id,email,name,note,subscribed_to_emails,complimentary_plan,stripe_customer_id,created_at,deleted_at,labels,tiers/ ) ;
2022-04-04 15:45:30 +03:00
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' ) ) ;
2022-10-21 12:45:02 +03:00
should . exist ( csv . data . find ( row => row . tiers . length > 0 ) ) ;
2022-05-19 19:26:29 +03:00
should . exist ( csv . data . find ( row => row . labels . length > 0 ) ) ;
2022-04-04 15:45:30 +03:00
} ) ;
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
} ) ;
2022-10-21 12:45:02 +03:00
res . text . should . match ( /id,email,name,note,subscribed_to_emails,complimentary_plan,stripe_customer_id,created_at,deleted_at,labels,tiers/ ) ;
2022-04-04 15:45:30 +03:00
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' ) ) ;
2022-10-21 12:45:02 +03:00
// note that this member doesn't have tiers
2022-05-19 19:26:29 +03:00
should . exist ( csv . data . find ( row => row . labels . length > 0 ) ) ;
2022-04-04 15:45:30 +03:00
} ) ;
// 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-05-18 18:28:00 +03:00
it ( 'Can filter on tier slug' , async function ( ) {
2022-07-15 13:16:06 +03:00
await agent
2022-05-18 18:28:00 +03:00
. get ( '/members/?include=tiers&filter=tier:default-product' )
. expectStatus ( 200 )
. matchBodySnapshot ( {
2022-07-15 13:16:06 +03:00
members : new Array ( 8 ) . fill ( buildMemberMatcherShallowIncludesWithTiers ( ) )
2022-05-18 18:28:00 +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
} ) ;
2022-05-20 14:40:55 +03:00
describe ( 'Members API Bulk operations' , function ( ) {
beforeEach ( async function ( ) {
agent = await agentProvider . getAdminAPIAgent ( ) ;
await fixtureManager . init ( 'newsletters' , 'members:newsletters' ) ;
await agent . loginAsOwner ( ) ;
mockManager . mockStripe ( ) ;
mockManager . mockMail ( ) ;
} ) ;
afterEach ( function ( ) {
mockManager . restore ( ) ;
} ) ;
it ( 'Can bulk unsubscribe members with filter' , async function ( ) {
// This member has 2 subscriptions
const member = fixtureManager . get ( 'members' , 4 ) ;
const newsletterCount = 2 ;
const model = await models . Member . findOne ( { id : member . id } , { withRelated : 'newsletters' } ) ;
should ( model . relations . newsletters . models . length ) . equal ( newsletterCount , 'This test requires a member with 2 or more newsletters' ) ;
await agent
. put ( ` /members/bulk/?filter=id: ${ member . id } ` )
. body ( { bulk : {
action : 'unsubscribe'
} } )
. expectStatus ( 200 )
. matchBodySnapshot ( {
bulk : {
meta : {
stats : {
// Should contain the count of members, not the newsletter count!
successful : 1 ,
unsuccessful : 0
} ,
unsuccessfulData : [ ] ,
errors : [ ]
}
}
} )
. matchHeaderSnapshot ( {
etag : anyEtag
} ) ;
2022-07-15 13:16:06 +03:00
2022-05-20 14:40:55 +03:00
const updatedModel = await models . Member . findOne ( { id : member . id } , { withRelated : 'newsletters' } ) ;
should ( updatedModel . relations . newsletters . models . length ) . equal ( 0 , 'This member should be unsubscribed from all newsletters' ) ;
// When we do it again, we should still receive a count of 1, because we unsubcribed one member (who happens to be already unsubscribed)
await agent
. put ( ` /members/bulk/?filter=id: ${ member . id } ` )
. body ( { bulk : {
action : 'unsubscribe'
} } )
. expectStatus ( 200 )
. matchBodySnapshot ( {
bulk : {
meta : {
stats : {
// Should contain the count of members, not the newsletter count!
successful : 1 ,
unsuccessful : 0
} ,
unsuccessfulData : [ ] ,
errors : [ ]
}
}
} )
. matchHeaderSnapshot ( {
etag : anyEtag
} ) ;
} ) ;
it ( 'Can bulk unsubscribe members with deprecated subscribed filter' , async function ( ) {
await agent
. put ( ` /members/bulk/?filter=subscribed:false ` )
. body ( { bulk : {
action : 'unsubscribe'
} } )
. expectStatus ( 200 )
. matchBodySnapshot ( {
bulk : {
meta : {
stats : {
successful : 2 , // We have two members who are subscribed to an inactive newsletter
unsuccessful : 0
} ,
unsuccessfulData : [ ] ,
errors : [ ]
}
}
} )
. matchHeaderSnapshot ( {
etag : anyEtag
} ) ;
} ) ;
it ( 'Can bulk unsubscribe members with deprecated subscribed filter (actual)' , async function ( ) {
// This member is subscribed to an inactive newsletter
const ignoredMember = fixtureManager . get ( 'members' , 6 ) ;
await agent
. put ( ` /members/bulk/?filter=subscribed:true ` )
. body ( { bulk : {
action : 'unsubscribe'
} } )
. expectStatus ( 200 )
. matchBodySnapshot ( {
bulk : {
meta : {
stats : {
successful : 6 , // not 7 because members subscribed to an inactive newsletter aren't subscribed (newsletter fixture[2])
unsuccessful : 0
} ,
unsuccessfulData : [ ] ,
errors : [ ]
}
}
} )
. matchHeaderSnapshot ( {
etag : anyEtag
} ) ;
const allMembers = await models . Member . findAll ( { withRelated : 'newsletters' } ) ;
for ( const model of allMembers ) {
if ( model . id === ignoredMember . id ) {
continue ;
}
should ( model . relations . newsletters . models . length ) . equal ( 0 , 'This member should be unsubscribed from all newsletters' ) ;
}
} ) ;
it ( 'Can bulk delete a label from members' , async function ( ) {
await agent
. put ( ` /members/bulk/?all=true ` )
. body ( { bulk : {
action : 'removeLabel' ,
meta : {
label : {
// Note! this equals DataGenerator.Content.labels[2]
// the index is different in the fixtureManager
id : fixtureManager . get ( 'labels' , 1 ) . id
}
}
} } )
. expectStatus ( 200 )
. matchBodySnapshot ( {
bulk : {
meta : {
stats : {
successful : 2 ,
unsuccessful : 0
} ,
unsuccessfulData : [ ] ,
errors : [ ]
}
}
} )
. matchHeaderSnapshot ( {
etag : anyEtag
} ) ;
await agent
. put ( ` /members/bulk/?all=true ` )
. body ( { bulk : {
action : 'removeLabel' ,
meta : {
label : {
id : fixtureManager . get ( 'labels' , 0 ) . id
}
}
} } )
. expectStatus ( 200 )
. matchBodySnapshot ( {
bulk : {
meta : {
stats : {
successful : 1 ,
unsuccessful : 0
} ,
unsuccessfulData : [ ] ,
errors : [ ]
}
}
} )
. matchHeaderSnapshot ( {
etag : anyEtag
} ) ;
} ) ;
it ( ` Doesn't delete labels apart from the passed label id ` , async function ( ) {
const member = fixtureManager . get ( 'members' , 1 ) ;
// Manually add 2 labels to a member
await models . Member . edit ( { labels : [ { name : 'first-tag' } , { name : 'second-tag' } ] } , { id : member . id } ) ;
const model = await models . Member . findOne ( { id : member . id } , { withRelated : 'labels' } ) ;
should ( model . relations . labels . models . map ( m => m . get ( 'name' ) ) ) . match ( [ 'first-tag' , 'second-tag' ] ) ;
const firstId = model . relations . labels . models [ 0 ] . id ;
const secondId = model . relations . labels . models [ 1 ] . id ;
// Delete first label only
await agent
. put ( ` /members/bulk/?all=true ` )
. body ( { bulk : {
action : 'removeLabel' ,
meta : {
label : {
id : secondId
}
}
} } )
. expectStatus ( 200 )
. matchBodySnapshot ( {
bulk : {
meta : {
stats : {
successful : 1 ,
unsuccessful : 0
} ,
unsuccessfulData : [ ] ,
errors : [ ]
}
}
} )
. matchHeaderSnapshot ( {
etag : anyEtag
} ) ;
const updatedModel = await models . Member . findOne ( { id : member . id } , { withRelated : 'labels' } ) ;
should ( updatedModel . relations . labels . models . map ( m => m . id ) ) . match ( [ firstId ] ) ;
} ) ;
it ( 'Can bulk delete a label from members with filters' , async function ( ) {
const member1 = fixtureManager . get ( 'members' , 0 ) ;
const member2 = fixtureManager . get ( 'members' , 1 ) ;
// Manually add 2 labels to a member
await models . Member . edit ( { labels : [ { name : 'first-tag' } , { name : 'second-tag' } ] } , { id : member1 . id } ) ;
const model1 = await models . Member . findOne ( { id : member1 . id } , { withRelated : 'labels' } ) ;
should ( model1 . relations . labels . models . map ( m => m . get ( 'name' ) ) ) . match ( [ 'first-tag' , 'second-tag' ] ) ;
const firstId = model1 . relations . labels . models [ 0 ] . id ;
const secondId = model1 . relations . labels . models [ 1 ] . id ;
await models . Member . edit ( { labels : [ { name : 'first-tag' } , { name : 'second-tag' } ] } , { id : member2 . id } ) ;
const model2 = await models . Member . findOne ( { id : member2 . id } , { withRelated : 'labels' } ) ;
should ( model2 . relations . labels . models . map ( m => m . id ) ) . match ( [ firstId , secondId ] ) ;
await agent
. put ( ` /members/bulk/?filter=id: ${ member1 . id } ` )
. body ( { bulk : {
action : 'removeLabel' ,
meta : {
label : {
// Note! this equals DataGenerator.Content.labels[2]
// the index is different in the fixtureManager
id : firstId
}
}
} } )
. expectStatus ( 200 )
. matchBodySnapshot ( {
bulk : {
meta : {
stats : {
successful : 1 ,
unsuccessful : 0
} ,
unsuccessfulData : [ ] ,
errors : [ ]
}
}
} )
. matchHeaderSnapshot ( {
etag : anyEtag
} ) ;
const updatedModel1 = await models . Member . findOne ( { id : member1 . id } , { withRelated : 'labels' } ) ;
should ( updatedModel1 . relations . labels . models . map ( m => m . id ) ) . match ( [ secondId ] ) ;
const updatedModel2 = await models . Member . findOne ( { id : member2 . id } , { withRelated : 'labels' } ) ;
should ( updatedModel2 . relations . labels . models . map ( m => m . id ) ) . match ( [ firstId , secondId ] ) ;
} ) ;
} ) ;