mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-28 13:22:39 +03:00
e73b16979f
refs https://github.com/TryGhost/Arch/issues/87 - The Members Admin API and members.* webhooks were returning too many fields in the nested `newsletters` objects. There was no "allowlist" serializer for the newsletter object, which meant every time we add a new field to the database we would unintentionally return extra fields without a second thought. - With this change only following fields will be returned with `members[x].newsletters[x]`: 'id', 'name', 'description', 'status'
3281 lines
112 KiB
JavaScript
3281 lines
112 KiB
JavaScript
const {agentProvider, mockManager, fixtureManager, matchers, regexes} = require('../../utils/e2e-framework');
|
|
const {anyContentVersion, anyEtag, anyObjectId, anyUuid, anyISODateTime, anyISODate, anyString, anyArray, anyLocationFor, anyContentLength, anyErrorId, anyObject} = matchers;
|
|
const {queryStringToken} = regexes;
|
|
const ObjectId = require('bson-objectid').default;
|
|
|
|
const assert = require('assert/strict');
|
|
const nock = require('nock');
|
|
const sinon = require('sinon');
|
|
const should = require('should');
|
|
|
|
const testUtils = require('../../utils');
|
|
const configUtils = require('../../utils/configUtils');
|
|
|
|
const Papa = require('papaparse');
|
|
|
|
const models = require('../../../core/server/models');
|
|
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');
|
|
const settingsCache = require('../../../core/shared/settings-cache');
|
|
const DomainEvents = require('@tryghost/domain-events');
|
|
const logging = require('@tryghost/logging');
|
|
const {stripeMocker} = require('../../utils/e2e-framework-mock-manager');
|
|
|
|
/**
|
|
* Assert that haystack and needles match, ignoring the order.
|
|
*/
|
|
function matchArrayWithoutOrder(haystack, needles) {
|
|
// Order shouldn't matter here
|
|
for (const a of needles) {
|
|
haystack.should.matchAny(a);
|
|
}
|
|
|
|
assert.equal(haystack.length, needles.length, `Expected ${needles.length} items, but got ${haystack.length}`);
|
|
}
|
|
|
|
async function assertMemberEvents({eventType, memberId, asserts}) {
|
|
const events = await models[eventType].where('member_id', memberId).fetchAll();
|
|
const eventsJSON = events.map(e => e.toJSON());
|
|
|
|
// Order shouldn't matter here
|
|
for (const a of asserts) {
|
|
eventsJSON.should.matchAny(a);
|
|
}
|
|
assert.equal(events.length, asserts.length, `Only ${asserts.length} ${eventType} should have been added.`);
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
async function getPaidProduct() {
|
|
return await models.Product.findOne({type: 'paid'});
|
|
}
|
|
|
|
async function getOtherPaidProduct() {
|
|
return (await models.Product.findAll({type: 'paid'})).models[0];
|
|
}
|
|
|
|
async function getNewsletters() {
|
|
return (await models.Newsletter.findAll({filter: 'status:active'})).models;
|
|
}
|
|
|
|
const newsletterSnapshot = {
|
|
id: anyObjectId
|
|
};
|
|
|
|
const attributionSnapshot = {
|
|
id: null
|
|
};
|
|
|
|
const subscriptionSnapshot = {
|
|
id: anyString,
|
|
start_date: anyString,
|
|
current_period_end: anyString,
|
|
price: {
|
|
id: anyString,
|
|
price_id: anyObjectId,
|
|
tier: {
|
|
id: anyString,
|
|
tier_id: anyObjectId
|
|
}
|
|
},
|
|
plan: {
|
|
id: anyString
|
|
},
|
|
customer: {
|
|
id: anyString
|
|
}
|
|
};
|
|
|
|
const tierSnapshot = {
|
|
id: anyObjectId,
|
|
created_at: anyISODateTime,
|
|
updated_at: anyISODateTime,
|
|
monthly_price_id: anyString,
|
|
yearly_price_id: anyString
|
|
};
|
|
|
|
const subscriptionSnapshotWithTier = {
|
|
...subscriptionSnapshot,
|
|
tier: tierSnapshot
|
|
};
|
|
|
|
function buildMemberWithoutIncludesSnapshot(options) {
|
|
return {
|
|
id: anyObjectId,
|
|
uuid: anyUuid,
|
|
created_at: anyISODateTime,
|
|
updated_at: anyISODateTime,
|
|
newsletters: new Array(options.newsletters).fill(newsletterSnapshot)
|
|
};
|
|
}
|
|
|
|
function buildMemberWithIncludesSnapshot(options) {
|
|
return {
|
|
id: anyObjectId,
|
|
uuid: anyUuid,
|
|
created_at: anyISODateTime,
|
|
updated_at: anyISODateTime,
|
|
attribution: attributionSnapshot,
|
|
newsletters: new Array(options.newsletters).fill(newsletterSnapshot),
|
|
subscriptions: anyArray,
|
|
labels: anyArray
|
|
};
|
|
}
|
|
|
|
const tierMatcher = {
|
|
id: anyObjectId,
|
|
created_at: anyISODateTime,
|
|
updated_at: anyISODateTime,
|
|
monthly_price_id: anyObjectId,
|
|
yearly_price_id: anyObjectId
|
|
};
|
|
|
|
const memberMatcherShallowIncludes = {
|
|
id: anyObjectId,
|
|
uuid: anyUuid,
|
|
created_at: anyISODateTime,
|
|
updated_at: anyISODateTime,
|
|
subscriptions: anyArray,
|
|
labels: anyArray
|
|
};
|
|
|
|
/**
|
|
*
|
|
* @param {number} tiersCount
|
|
* @param {number} newsletterCount
|
|
* @returns
|
|
*/
|
|
const buildMemberMatcherShallowIncludesWithTiers = (tiersCount, newsletterCount) => {
|
|
const matcher = {
|
|
...memberMatcherShallowIncludes
|
|
};
|
|
|
|
if (tiersCount !== undefined) {
|
|
matcher.tiers = new Array(tiersCount).fill(tierMatcher);
|
|
}
|
|
|
|
if (newsletterCount !== undefined) {
|
|
matcher.newsletters = new Array(newsletterCount).fill(newsletterSnapshot);
|
|
}
|
|
|
|
return matcher;
|
|
};
|
|
|
|
let agent;
|
|
|
|
describe('Members API without Stripe', function () {
|
|
before(async function () {
|
|
agent = await agentProvider.getAdminAPIAgent();
|
|
await fixtureManager.init();
|
|
await agent.loginAsOwner();
|
|
|
|
await agent
|
|
.delete('/settings/stripe/connect/')
|
|
.expectStatus(204);
|
|
});
|
|
|
|
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({
|
|
'content-version': anyContentVersion,
|
|
etag: anyEtag
|
|
})
|
|
.matchBodySnapshot({
|
|
errors: [{
|
|
id: anyErrorId
|
|
}]
|
|
});
|
|
});
|
|
});
|
|
|
|
// 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();
|
|
});
|
|
|
|
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/',
|
|
type: 'post',
|
|
referrerSource: null,
|
|
referrerMedium: null,
|
|
referrerUrl: null
|
|
})
|
|
});
|
|
|
|
const absoluteUrl = urlService.getUrlByResourceId(post.id, {absolute: true});
|
|
|
|
await agent
|
|
.get(`/members/${member.id}/`)
|
|
.expectStatus(200)
|
|
.matchBodySnapshot({
|
|
members: new Array(1).fill(buildMemberMatcherShallowIncludesWithTiers(0, 2))
|
|
})
|
|
.matchHeaderSnapshot({
|
|
'content-version': anyContentVersion,
|
|
etag: anyEtag
|
|
})
|
|
.expect(({body}) => {
|
|
should(body.members[0].attribution).eql({
|
|
id: post.id,
|
|
url: absoluteUrl,
|
|
type: 'post',
|
|
title: post.get('title'),
|
|
referrer_source: null,
|
|
referrer_medium: null,
|
|
referrer_url: null
|
|
});
|
|
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/',
|
|
type: 'page',
|
|
referrerSource: null,
|
|
referrerMedium: null,
|
|
referrerUrl: null
|
|
})
|
|
});
|
|
|
|
const absoluteUrl = urlService.getUrlByResourceId(post.id, {absolute: true});
|
|
|
|
await agent
|
|
.get(`/members/${member.id}/`)
|
|
.expectStatus(200)
|
|
.matchBodySnapshot({
|
|
members: new Array(1).fill(buildMemberMatcherShallowIncludesWithTiers(0, 2))
|
|
})
|
|
.matchHeaderSnapshot({
|
|
'content-version': anyContentVersion,
|
|
etag: anyEtag
|
|
})
|
|
.expect(({body}) => {
|
|
should(body.members[0].attribution).eql({
|
|
id: post.id,
|
|
url: absoluteUrl,
|
|
type: 'page',
|
|
title: post.get('title'),
|
|
referrer_source: null,
|
|
referrer_medium: null,
|
|
referrer_url: null
|
|
});
|
|
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/',
|
|
type: 'tag',
|
|
referrerSource: null,
|
|
referrerMedium: null,
|
|
referrerUrl: null
|
|
})
|
|
});
|
|
|
|
const absoluteUrl = urlService.getUrlByResourceId(tag.id, {absolute: true});
|
|
|
|
await agent
|
|
.get(`/members/${member.id}/`)
|
|
.expectStatus(200)
|
|
.matchBodySnapshot({
|
|
members: new Array(1).fill(buildMemberMatcherShallowIncludesWithTiers(0, 2))
|
|
})
|
|
.matchHeaderSnapshot({
|
|
'content-version': anyContentVersion,
|
|
etag: anyEtag
|
|
})
|
|
.expect(({body}) => {
|
|
should(body.members[0].attribution).eql({
|
|
id: tag.id,
|
|
url: absoluteUrl,
|
|
type: 'tag',
|
|
title: tag.get('name'),
|
|
referrer_source: null,
|
|
referrer_medium: null,
|
|
referrer_url: null
|
|
});
|
|
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/',
|
|
type: 'author',
|
|
referrerSource: null,
|
|
referrerMedium: null,
|
|
referrerUrl: null
|
|
})
|
|
});
|
|
|
|
const absoluteUrl = urlService.getUrlByResourceId(author.id, {absolute: true});
|
|
|
|
await agent
|
|
.get(`/members/${member.id}/`)
|
|
.expectStatus(200)
|
|
.matchBodySnapshot({
|
|
members: new Array(1).fill(buildMemberMatcherShallowIncludesWithTiers(0, 2))
|
|
})
|
|
.matchHeaderSnapshot({
|
|
'content-version': anyContentVersion,
|
|
etag: anyEtag
|
|
})
|
|
.expect(({body}) => {
|
|
should(body.members[0].attribution).eql({
|
|
id: author.id,
|
|
url: absoluteUrl,
|
|
type: 'author',
|
|
title: author.get('name'),
|
|
referrer_source: null,
|
|
referrer_medium: null,
|
|
referrer_url: null
|
|
});
|
|
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/',
|
|
type: 'url',
|
|
referrerSource: null,
|
|
referrerMedium: null,
|
|
referrerUrl: null
|
|
})
|
|
});
|
|
|
|
const absoluteUrl = urlUtils.createUrl('/a-static-page/', true);
|
|
|
|
await agent
|
|
.get(`/members/${member.id}/`)
|
|
.expectStatus(200)
|
|
.matchBodySnapshot({
|
|
members: new Array(1).fill(buildMemberMatcherShallowIncludesWithTiers(0, 2))
|
|
})
|
|
.matchHeaderSnapshot({
|
|
'content-version': anyContentVersion,
|
|
etag: anyEtag
|
|
})
|
|
.expect(({body}) => {
|
|
should(body.members[0].attribution).eql({
|
|
id: null,
|
|
url: absoluteUrl,
|
|
type: 'url',
|
|
title: '/a-static-page/',
|
|
referrer_source: null,
|
|
referrer_medium: null,
|
|
referrer_url: null
|
|
});
|
|
signupAttributions.push(body.members[0].attribution);
|
|
});
|
|
});
|
|
|
|
// Activity feed
|
|
it('Returns sign up attributions of all types in activity feed', async function () {
|
|
// Check activity feed
|
|
await agent
|
|
.get(`/members/events/?filter=type:signup_event`)
|
|
.expectStatus(200)
|
|
.matchHeaderSnapshot({
|
|
'content-version': anyContentVersion,
|
|
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);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Members API', function () {
|
|
let newsletters;
|
|
let emailMockReceiver;
|
|
|
|
before(async function () {
|
|
agent = await agentProvider.getAdminAPIAgent();
|
|
await fixtureManager.init('posts', 'newsletters', 'members:newsletters', 'comments', 'redirects', 'clicks');
|
|
await agent.loginAsOwner();
|
|
|
|
newsletters = await getNewsletters();
|
|
});
|
|
|
|
beforeEach(function () {
|
|
mockManager.mockStripe();
|
|
emailMockReceiver = mockManager.mockMail();
|
|
});
|
|
|
|
afterEach(function () {
|
|
mockManager.restore();
|
|
});
|
|
|
|
// List Members
|
|
|
|
it('Can browse', async function () {
|
|
await agent
|
|
.get('/members/')
|
|
.expectStatus(200)
|
|
.matchBodySnapshot({
|
|
members: [
|
|
buildMemberMatcherShallowIncludesWithTiers(undefined, 0),
|
|
buildMemberMatcherShallowIncludesWithTiers(undefined, 0),
|
|
buildMemberMatcherShallowIncludesWithTiers(undefined, 1),
|
|
buildMemberMatcherShallowIncludesWithTiers(undefined, 2),
|
|
buildMemberMatcherShallowIncludesWithTiers(undefined, 1),
|
|
buildMemberMatcherShallowIncludesWithTiers(undefined, 1),
|
|
buildMemberMatcherShallowIncludesWithTiers(undefined, 1),
|
|
buildMemberMatcherShallowIncludesWithTiers(undefined, 1)
|
|
]
|
|
})
|
|
.matchHeaderSnapshot({
|
|
'content-version': anyContentVersion,
|
|
etag: anyEtag
|
|
});
|
|
});
|
|
|
|
it('Can browse with filter', async function () {
|
|
await agent
|
|
.get('/members/?filter=label:label-1')
|
|
.expectStatus(200)
|
|
.matchBodySnapshot({
|
|
members: new Array(1).fill(buildMemberMatcherShallowIncludesWithTiers(undefined, 1))
|
|
})
|
|
.matchHeaderSnapshot({
|
|
'content-version': anyContentVersion,
|
|
etag: anyEtag
|
|
});
|
|
});
|
|
|
|
it('Can filter by signup attribution', async function () {
|
|
await agent
|
|
.get('/members/?filter=signup:' + fixtureManager.get('posts', 0).id)
|
|
.expectStatus(200)
|
|
.matchBodySnapshot({
|
|
members: [
|
|
buildMemberMatcherShallowIncludesWithTiers(undefined, 0),
|
|
buildMemberMatcherShallowIncludesWithTiers(undefined, 1),
|
|
buildMemberMatcherShallowIncludesWithTiers(undefined, 1)
|
|
]
|
|
})
|
|
.matchHeaderSnapshot({
|
|
'content-version': anyContentVersion,
|
|
etag: anyEtag
|
|
});
|
|
});
|
|
|
|
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(buildMemberMatcherShallowIncludesWithTiers(undefined, 1))
|
|
})
|
|
.matchHeaderSnapshot({
|
|
'content-version': anyContentVersion,
|
|
etag: anyEtag
|
|
});
|
|
});
|
|
|
|
it('Can browse with search', async function () {
|
|
await agent
|
|
.get('/members/?search=member1')
|
|
.expectStatus(200)
|
|
.matchBodySnapshot({
|
|
members: new Array(1).fill(buildMemberMatcherShallowIncludesWithTiers(undefined, 1))
|
|
})
|
|
.matchHeaderSnapshot({
|
|
'content-version': anyContentVersion,
|
|
etag: anyEtag
|
|
});
|
|
});
|
|
|
|
it('Can filter by paid status', async function () {
|
|
await agent
|
|
.get('/members/?filter=status:paid')
|
|
.expectStatus(200)
|
|
.matchBodySnapshot({
|
|
members: [
|
|
buildMemberMatcherShallowIncludesWithTiers(undefined, 0),
|
|
buildMemberMatcherShallowIncludesWithTiers(undefined, 0),
|
|
buildMemberMatcherShallowIncludesWithTiers(undefined, 2),
|
|
buildMemberMatcherShallowIncludesWithTiers(undefined, 1),
|
|
buildMemberMatcherShallowIncludesWithTiers(undefined, 1)
|
|
]
|
|
})
|
|
.matchHeaderSnapshot({
|
|
'content-version': anyContentVersion,
|
|
etag: anyEtag
|
|
});
|
|
});
|
|
|
|
it('Can filter by tier id', async function () {
|
|
const products = await getPaidProduct();
|
|
await agent
|
|
.get(`/members/?filter=tier_id:[${products.toJSON().id}]`)
|
|
.expectStatus(200)
|
|
.matchBodySnapshot({
|
|
members: [
|
|
buildMemberMatcherShallowIncludesWithTiers(undefined, 0),
|
|
buildMemberMatcherShallowIncludesWithTiers(undefined, 2),
|
|
buildMemberMatcherShallowIncludesWithTiers(undefined, 1),
|
|
buildMemberMatcherShallowIncludesWithTiers(undefined, 1)
|
|
]
|
|
})
|
|
.matchHeaderSnapshot({
|
|
'content-version': anyContentVersion,
|
|
etag: anyEtag
|
|
});
|
|
});
|
|
|
|
it('Can filter using contains operators', async function () {
|
|
await agent
|
|
.get(`/members/?filter=name:~'Venkman'`)
|
|
.expectStatus(200)
|
|
.matchBodySnapshot({
|
|
members: new Array(1).fill(buildMemberMatcherShallowIncludesWithTiers(undefined, 0))
|
|
})
|
|
.matchHeaderSnapshot({
|
|
'content-version': anyContentVersion,
|
|
etag: anyEtag
|
|
});
|
|
});
|
|
|
|
it('Can ignore any unknown includes', async function () {
|
|
await agent
|
|
.get('/members/?filter=status:paid&include=emailRecipients')
|
|
.expectStatus(200)
|
|
.matchBodySnapshot({
|
|
members: [
|
|
buildMemberMatcherShallowIncludesWithTiers(undefined, 0),
|
|
buildMemberMatcherShallowIncludesWithTiers(undefined, 0),
|
|
buildMemberMatcherShallowIncludesWithTiers(undefined, 2),
|
|
buildMemberMatcherShallowIncludesWithTiers(undefined, 1),
|
|
buildMemberMatcherShallowIncludesWithTiers(undefined, 1)
|
|
]
|
|
})
|
|
.matchHeaderSnapshot({
|
|
'content-version': anyContentVersion,
|
|
etag: anyEtag
|
|
});
|
|
});
|
|
|
|
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': anyContentLength,
|
|
'content-version': anyContentVersion
|
|
})
|
|
.matchBodySnapshot({
|
|
members: [
|
|
buildMemberMatcherShallowIncludesWithTiers(undefined, 1),
|
|
buildMemberMatcherShallowIncludesWithTiers(undefined, 1),
|
|
buildMemberMatcherShallowIncludesWithTiers(undefined, 1),
|
|
buildMemberMatcherShallowIncludesWithTiers(undefined, 1),
|
|
buildMemberMatcherShallowIncludesWithTiers(undefined, 2),
|
|
buildMemberMatcherShallowIncludesWithTiers(undefined, 1),
|
|
buildMemberMatcherShallowIncludesWithTiers(undefined, 0),
|
|
buildMemberMatcherShallowIncludesWithTiers(undefined, 0)
|
|
]
|
|
})
|
|
.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({
|
|
etag: anyEtag,
|
|
'content-length': anyContentLength,
|
|
'content-version': anyContentVersion
|
|
})
|
|
.matchBodySnapshot({
|
|
members: [
|
|
buildMemberMatcherShallowIncludesWithTiers(undefined, 1),
|
|
buildMemberMatcherShallowIncludesWithTiers(undefined, 1),
|
|
buildMemberMatcherShallowIncludesWithTiers(undefined, 1),
|
|
buildMemberMatcherShallowIncludesWithTiers(undefined, 1),
|
|
buildMemberMatcherShallowIncludesWithTiers(undefined, 2),
|
|
buildMemberMatcherShallowIncludesWithTiers(undefined, 1),
|
|
buildMemberMatcherShallowIncludesWithTiers(undefined, 0),
|
|
buildMemberMatcherShallowIncludesWithTiers(undefined, 0)
|
|
]
|
|
})
|
|
.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.');
|
|
});
|
|
});
|
|
|
|
it('Search by case-insensitive name egg receives member with name Mr Egg', async function () {
|
|
await agent
|
|
.get('members/?search=egg')
|
|
.expectStatus(200)
|
|
.matchBodySnapshot({
|
|
members: [buildMemberMatcherShallowIncludesWithTiers(undefined, 1)]
|
|
})
|
|
.matchHeaderSnapshot({
|
|
'content-version': anyContentVersion,
|
|
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: [buildMemberMatcherShallowIncludesWithTiers(undefined, 1)]
|
|
})
|
|
.matchHeaderSnapshot({
|
|
'content-version': anyContentVersion,
|
|
etag: anyEtag
|
|
});
|
|
});
|
|
|
|
it('Search for paid members retrieves member with email paid@test.com', async function () {
|
|
await agent
|
|
.get('members/?search=egon&paid=true')
|
|
.expectStatus(200)
|
|
.matchBodySnapshot({
|
|
members: [buildMemberMatcherShallowIncludesWithTiers(undefined, 1)]
|
|
})
|
|
.matchHeaderSnapshot({
|
|
'content-version': anyContentVersion,
|
|
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({
|
|
'content-version': anyContentVersion,
|
|
etag: anyEtag
|
|
})
|
|
.matchBodySnapshot({
|
|
members: []
|
|
});
|
|
});
|
|
|
|
// Read a member
|
|
|
|
it('Can read', async function () {
|
|
await agent
|
|
.get(`/members/${testUtils.DataGenerator.Content.members[0].id}/`)
|
|
.expectStatus(200)
|
|
.matchBodySnapshot({
|
|
members: new Array(1).fill(buildMemberMatcherShallowIncludesWithTiers(0, 1))
|
|
})
|
|
.matchHeaderSnapshot({
|
|
'content-version': anyContentVersion,
|
|
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({
|
|
members: new Array(1).fill(buildMemberMatcherShallowIncludesWithTiers(0, 1))
|
|
})
|
|
.matchHeaderSnapshot({
|
|
'content-version': anyContentVersion,
|
|
etag: anyEtag
|
|
});
|
|
});
|
|
|
|
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(buildMemberMatcherShallowIncludesWithTiers(0, 1))
|
|
})
|
|
.matchHeaderSnapshot({
|
|
'content-version': anyContentVersion,
|
|
etag: anyEtag
|
|
});
|
|
});
|
|
|
|
// Create a member
|
|
|
|
it('Can add', async function () {
|
|
const member = {
|
|
name: 'test',
|
|
email: 'memberTestAdd@test.com',
|
|
note: 'test note',
|
|
newsletters: [],
|
|
labels: ['test-label']
|
|
};
|
|
|
|
const {body} = await agent
|
|
.post(`/members/`)
|
|
.body({members: [member]})
|
|
.expectStatus(201)
|
|
.matchBodySnapshot({
|
|
members: new Array(1).fill(buildMemberMatcherShallowIncludesWithTiers(0, 0))
|
|
})
|
|
.matchHeaderSnapshot({
|
|
'content-version': anyContentVersion,
|
|
etag: anyEtag,
|
|
location: anyLocationFor('members')
|
|
});
|
|
const newMember = body.members[0];
|
|
|
|
// Cannot add same member twice
|
|
const loggingStub = sinon.stub(logging, 'error');
|
|
await agent
|
|
.post(`/members/`)
|
|
.body({members: [member]})
|
|
.expectStatus(422);
|
|
sinon.assert.calledOnce(loggingStub);
|
|
|
|
await assertMemberEvents({
|
|
eventType: 'MemberStatusEvent',
|
|
memberId: newMember.id,
|
|
asserts: [
|
|
{
|
|
from_status: null,
|
|
to_status: 'free'
|
|
}
|
|
]
|
|
});
|
|
});
|
|
|
|
it('Can add a member and trigger host email verification limits', async function () {
|
|
configUtils.set('hostSettings:emailVerification', {
|
|
apiThreshold: 0,
|
|
adminThreshold: 1,
|
|
importThreshold: 0,
|
|
verified: false,
|
|
escalationAddress: 'test@example.com'
|
|
});
|
|
|
|
assert.ok(!settingsCache.get('email_verification_required'), 'Email verification should NOT be required');
|
|
|
|
const member = {
|
|
name: 'pass verification',
|
|
email: 'memberPassVerifivation@test.com'
|
|
};
|
|
|
|
const {body: passBody} = await agent
|
|
.post(`/members/`)
|
|
.body({members: [member]})
|
|
.expectStatus(201)
|
|
.matchBodySnapshot({
|
|
members: new Array(1).fill(buildMemberMatcherShallowIncludesWithTiers(0, 2))
|
|
})
|
|
.matchHeaderSnapshot({
|
|
'content-version': anyContentVersion,
|
|
etag: anyEtag,
|
|
location: anyLocationFor('members')
|
|
});
|
|
const memberPassVerification = passBody.members[0];
|
|
|
|
await DomainEvents.allSettled();
|
|
assert.ok(!settingsCache.get('email_verification_required'), 'Email verification should NOT be required');
|
|
|
|
const memberFailLimit = {
|
|
name: 'fail verification',
|
|
email: 'memberFailVerifivation@test.com'
|
|
};
|
|
|
|
const {body: failBody} = await agent
|
|
.post(`/members/`)
|
|
.body({members: [memberFailLimit]})
|
|
.expectStatus(201)
|
|
.matchBodySnapshot({
|
|
members: new Array(1).fill(buildMemberMatcherShallowIncludesWithTiers(0, 2))
|
|
})
|
|
.matchHeaderSnapshot({
|
|
'content-version': anyContentVersion,
|
|
etag: anyEtag,
|
|
location: anyLocationFor('members')
|
|
});
|
|
const memberFailVerification = failBody.members[0];
|
|
|
|
await DomainEvents.allSettled();
|
|
assert.ok(settingsCache.get('email_verification_required'), 'Email verification should be required');
|
|
|
|
mockManager.assert.sentEmail({
|
|
subject: 'Email needs verification'
|
|
});
|
|
|
|
// state cleanup
|
|
await agent.delete(`/members/${memberPassVerification.id}`);
|
|
await agent.delete(`/members/${memberFailVerification.id}`);
|
|
|
|
await configUtils.restore();
|
|
});
|
|
|
|
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]
|
|
]
|
|
};
|
|
|
|
// Set site title to something with a special character to ensure subject line doesn't get escaped
|
|
// Refs https://github.com/TryGhost/Team/issues/2895
|
|
await agent.put('/settings/')
|
|
.body({
|
|
settings: [
|
|
{
|
|
key: 'title',
|
|
value: 'Ghost\'s Test Site'
|
|
}
|
|
]
|
|
})
|
|
.expectStatus(200);
|
|
|
|
const {body} = await agent
|
|
.post('/members/?send_email=true&email_type=signup')
|
|
.body({members: [member]})
|
|
.expectStatus(201)
|
|
.matchBodySnapshot({
|
|
members: [
|
|
buildMemberWithoutIncludesSnapshot({
|
|
newsletters: 2
|
|
})
|
|
]
|
|
})
|
|
.matchHeaderSnapshot({
|
|
'content-version': anyContentVersion,
|
|
etag: anyEtag,
|
|
location: anyString
|
|
});
|
|
|
|
const newMember = body.members[0];
|
|
|
|
emailMockReceiver
|
|
.assertSentEmailCount(1)
|
|
.matchHTMLSnapshot([{
|
|
pattern: queryStringToken('token'),
|
|
replacement: 'token=REPLACED_TOKEN'
|
|
}])
|
|
.matchPlaintextSnapshot([{
|
|
pattern: queryStringToken('token'),
|
|
replacement: 'token=REPLACED_TOKEN'
|
|
}])
|
|
.matchMetadataSnapshot();
|
|
|
|
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
|
|
}
|
|
]
|
|
});
|
|
|
|
// @TODO: do we really need to delete this member here?
|
|
await agent
|
|
.delete(`members/${body.members[0].id}/`)
|
|
.matchHeaderSnapshot({
|
|
'content-version': anyContentVersion,
|
|
etag: anyEtag
|
|
})
|
|
.expectStatus(204);
|
|
|
|
// There should be no MemberSubscribeEvent remaining.
|
|
await assertMemberEvents({
|
|
eventType: 'MemberSubscribeEvent',
|
|
memberId: newMember.id,
|
|
asserts: []
|
|
});
|
|
|
|
// Reset the site title to the default
|
|
await agent.put('/settings/')
|
|
.body({
|
|
settings: [
|
|
{
|
|
key: 'title',
|
|
value: 'Ghost'
|
|
}
|
|
]
|
|
})
|
|
.expectStatus(200);
|
|
});
|
|
|
|
it('Add should fail when passing incorrect email_type query parameter', async function () {
|
|
const newMember = {
|
|
name: 'test',
|
|
email: 'memberTestAdd@test.com'
|
|
};
|
|
|
|
const statusEventsBefore = await models.MemberStatusEvent.findAll();
|
|
|
|
sinon.stub(logging, 'error');
|
|
await agent
|
|
.post(`members/?send_email=true&email_type=lel`)
|
|
.body({members: [newMember]})
|
|
.expectStatus(422)
|
|
.matchHeaderSnapshot({
|
|
'content-version': anyContentVersion,
|
|
etag: anyEtag
|
|
})
|
|
.matchBodySnapshot({
|
|
errors: [{
|
|
id: anyErrorId
|
|
}]
|
|
});
|
|
|
|
const statusEvents = await models.MemberStatusEvent.findAll();
|
|
assert.equal(statusEvents.models.length, statusEventsBefore.models.length, 'No MemberStatusEvent should have been added after failing to create a subscription.');
|
|
});
|
|
|
|
// Edit a member
|
|
|
|
it('Can add complimentary subscription (out of date)', async function () {
|
|
const stripeService = require('../../../core/server/services/stripe');
|
|
const fakePrice = {
|
|
id: 'price_1',
|
|
product: '',
|
|
active: true,
|
|
nickname: 'Complimentary',
|
|
unit_amount: 0,
|
|
currency: 'usd',
|
|
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',
|
|
newsletters: [newsletters[0]]
|
|
};
|
|
|
|
const compedPayload = {
|
|
comped: true
|
|
};
|
|
|
|
const {body} = await agent
|
|
.post(`/members/`)
|
|
.body({members: [initialMember]})
|
|
.expectStatus(201)
|
|
.matchBodySnapshot({
|
|
members: new Array(1).fill(buildMemberMatcherShallowIncludesWithTiers(0, 1))
|
|
})
|
|
.matchHeaderSnapshot({
|
|
'content-version': anyContentVersion,
|
|
etag: anyEtag,
|
|
location: anyLocationFor('members')
|
|
});
|
|
|
|
const newMember = body.members[0];
|
|
|
|
await agent
|
|
.put(`/members/${newMember.id}/`)
|
|
.body({members: [compedPayload]})
|
|
.expectStatus(200)
|
|
.matchBodySnapshot({
|
|
members: new Array(1).fill(buildMemberMatcherShallowIncludesWithTiers(1, 1))
|
|
})
|
|
.matchHeaderSnapshot({
|
|
'content-version': anyContentVersion,
|
|
etag: anyEtag
|
|
});
|
|
|
|
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',
|
|
newsletter_id: newsletters[0].id
|
|
}]
|
|
});
|
|
});
|
|
|
|
it('Can add complimentary subscription by assigning a product to a member', async function () {
|
|
const initialMember = {
|
|
name: 'Name',
|
|
email: 'compedtest2@test.com',
|
|
newsletters: [newsletters[0]]
|
|
};
|
|
|
|
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,
|
|
tiers: [
|
|
{
|
|
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.tiers.length, 1, 'The member should have one product');
|
|
|
|
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',
|
|
newsletter_id: newsletters[0].id
|
|
}]
|
|
});
|
|
|
|
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',
|
|
newsletters: [newsletters[0]],
|
|
tiers: [
|
|
{
|
|
id: product.id
|
|
}
|
|
]
|
|
};
|
|
|
|
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');
|
|
assert.equal(newMember.tiers.length, 1, 'The member should have 1 product');
|
|
|
|
// Remove it
|
|
const removePayload = {
|
|
id: newMember.id,
|
|
email: newMember.email,
|
|
tiers: []
|
|
};
|
|
|
|
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.tiers.length, 0, 'The member should have 0 tiers');
|
|
|
|
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,
|
|
source: 'admin',
|
|
newsletter_id: newsletters[0].id
|
|
}
|
|
]
|
|
});
|
|
|
|
await assertMemberEvents({
|
|
eventType: 'MemberPaidSubscriptionEvent',
|
|
memberId: newMember.id,
|
|
asserts: []
|
|
});
|
|
});
|
|
|
|
it('Can create a new member with a product (complimentary)', async function () {
|
|
const product = await getPaidProduct();
|
|
const initialMember = {
|
|
name: 'Name',
|
|
email: 'compedtest4@test.com',
|
|
subscribed: true,
|
|
newsletters: [newsletters[0]],
|
|
tiers: [
|
|
{
|
|
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,
|
|
tiers: new Array(1).fill({
|
|
id: anyObjectId,
|
|
monthly_price_id: anyObjectId,
|
|
yearly_price_id: anyObjectId,
|
|
created_at: anyISODateTime,
|
|
updated_at: anyISODateTime
|
|
}),
|
|
newsletters: new Array(1).fill(newsletterSnapshot)
|
|
})
|
|
})
|
|
.matchHeaderSnapshot({
|
|
'content-version': anyContentVersion,
|
|
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,
|
|
currency: 'usd',
|
|
type: 'recurring',
|
|
recurring: {
|
|
interval: 'year'
|
|
}
|
|
};
|
|
|
|
const fakeSubscription = {
|
|
id: 'sub_2',
|
|
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
|
|
}]
|
|
}
|
|
};
|
|
|
|
const fakeCustomer = {
|
|
id: 'cus_1234',
|
|
name: 'Test Member',
|
|
email: 'create-member-comped-test@email.com',
|
|
subscriptions: {
|
|
type: 'list',
|
|
data: [fakeSubscription]
|
|
}
|
|
};
|
|
stripeMocker.customers.push(fakeCustomer);
|
|
stripeMocker.subscriptions.push(fakeSubscription);
|
|
stripeMocker.prices.push(fakePrice);
|
|
|
|
const initialMember = {
|
|
name: fakeCustomer.name,
|
|
email: fakeCustomer.email,
|
|
subscribed: true,
|
|
newsletters: [newsletters[0]],
|
|
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,
|
|
tiers: new Array(1).fill(tierMatcher),
|
|
newsletters: new Array(1).fill(newsletterSnapshot)
|
|
})
|
|
})
|
|
.matchHeaderSnapshot({
|
|
'content-version': anyContentVersion,
|
|
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
|
|
}]
|
|
});
|
|
});
|
|
|
|
let memberWithPaidSubscription;
|
|
|
|
it('Can create a member with an existing paid subscription', async function () {
|
|
const fakePrice = {
|
|
id: 'price_1',
|
|
product: 'product_1234',
|
|
active: true,
|
|
nickname: 'Paid',
|
|
unit_amount: 1200,
|
|
currency: 'usd',
|
|
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: [{
|
|
id: 'item_123',
|
|
price: fakePrice
|
|
}]
|
|
}
|
|
};
|
|
|
|
const fakeCustomer = {
|
|
id: 'cus_12345',
|
|
name: 'Test Member',
|
|
email: 'create-member-paid-test@email.com',
|
|
subscriptions: {
|
|
type: 'list',
|
|
data: [fakeSubscription]
|
|
}
|
|
};
|
|
|
|
stripeMocker.customers.push(fakeCustomer);
|
|
stripeMocker.subscriptions.push(fakeSubscription);
|
|
stripeMocker.prices.push(fakePrice);
|
|
|
|
const initialMember = {
|
|
name: fakeCustomer.name,
|
|
email: fakeCustomer.email,
|
|
subscribed: true,
|
|
newsletters: [newsletters[0]],
|
|
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,
|
|
tiers: new Array(1).fill(tierMatcher),
|
|
newsletters: new Array(1).fill(newsletterSnapshot)
|
|
})
|
|
})
|
|
.matchHeaderSnapshot({
|
|
'content-version': anyContentVersion,
|
|
etag: anyEtag,
|
|
location: anyLocationFor('members')
|
|
});
|
|
|
|
const newMember = body.members[0];
|
|
|
|
assert.equal(newMember.status, 'paid', 'The created member should have the paid status');
|
|
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');
|
|
|
|
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
|
|
}
|
|
]
|
|
});
|
|
|
|
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
|
|
});
|
|
|
|
// Save this member for the next tests
|
|
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 tiers while browsing
|
|
const {body: browseBody} = await agent.get(`/members/?search=${memberWithPaidSubscription.email}&include=tiers`);
|
|
assert.equal(browseBody.members.length, 1, 'The member was not found in browse');
|
|
const browseMember = browseBody.members[0];
|
|
|
|
// 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;
|
|
}
|
|
|
|
// 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');
|
|
});
|
|
|
|
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
|
|
}
|
|
]
|
|
};
|
|
|
|
sinon.stub(logging, 'error');
|
|
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: []
|
|
};
|
|
|
|
sinon.stub(logging, 'error');
|
|
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
|
|
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];
|
|
assert.equal(beforeMember.tiers.length, 2, 'The member should have two tiers now');
|
|
|
|
// 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);
|
|
});
|
|
|
|
it('Can edit by id', async function () {
|
|
const memberToChange = {
|
|
name: 'change me',
|
|
email: 'member2Change@test.com',
|
|
note: 'initial note',
|
|
newsletters: [
|
|
newsletters[0]
|
|
]
|
|
};
|
|
|
|
const memberChanged = {
|
|
name: 'changed',
|
|
email: 'cantChangeMe@test.com',
|
|
note: 'edited note',
|
|
newsletters: []
|
|
};
|
|
|
|
const {body} = await agent
|
|
.post(`/members/`)
|
|
.body({members: [memberToChange]})
|
|
.expectStatus(201)
|
|
.matchBodySnapshot({
|
|
members: new Array(1).fill(buildMemberMatcherShallowIncludesWithTiers(0, 1))
|
|
})
|
|
.matchHeaderSnapshot({
|
|
'content-version': anyContentVersion,
|
|
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: new Array(1).fill(buildMemberMatcherShallowIncludesWithTiers(0, 0))
|
|
})
|
|
.matchHeaderSnapshot({
|
|
'content-version': anyContentVersion,
|
|
etag: anyEtag
|
|
});
|
|
|
|
await assertMemberEvents({
|
|
eventType: 'MemberEmailChangeEvent',
|
|
memberId: newMember.id,
|
|
asserts: [{
|
|
from_email: memberToChange.email,
|
|
to_email: memberChanged.email
|
|
}]
|
|
});
|
|
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
|
|
}
|
|
]
|
|
});
|
|
});
|
|
|
|
// Internally a different error is thrown for newsletters/tiers 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({
|
|
'content-version': anyContentVersion,
|
|
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,
|
|
context: anyString
|
|
}]
|
|
})
|
|
.matchHeaderSnapshot({
|
|
'content-version': anyContentVersion,
|
|
etag: anyEtag
|
|
});
|
|
});
|
|
|
|
it('Can subscribe to a newsletter', async function () {
|
|
const clock = sinon.useFakeTimers(Date.now());
|
|
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(buildMemberMatcherShallowIncludesWithTiers(0, 1))
|
|
})
|
|
.matchHeaderSnapshot({
|
|
'content-version': anyContentVersion,
|
|
etag: anyEtag,
|
|
location: anyLocationFor('members')
|
|
});
|
|
const newMember = body.members[0];
|
|
const before = new Date();
|
|
before.setMilliseconds(0);
|
|
|
|
await assertMemberEvents({
|
|
eventType: 'MemberSubscribeEvent',
|
|
memberId: newMember.id,
|
|
asserts: [{
|
|
subscribed: true,
|
|
source: 'admin',
|
|
newsletter_id: newsletters[0].id,
|
|
created_at: before
|
|
}]
|
|
});
|
|
|
|
// Wait 5 seconds to guarantee event ordering
|
|
clock.tick(5000);
|
|
|
|
const after = new Date();
|
|
after.setMilliseconds(0);
|
|
|
|
await agent
|
|
.put(`/members/${newMember.id}/`)
|
|
.body({members: [memberChanged]})
|
|
.expectStatus(200)
|
|
.matchBodySnapshot({
|
|
members: new Array(1).fill(buildMemberMatcherShallowIncludesWithTiers(0, 1))
|
|
})
|
|
.matchHeaderSnapshot({
|
|
'content-version': anyContentVersion,
|
|
etag: anyEtag
|
|
});
|
|
|
|
await assertMemberEvents({
|
|
eventType: 'MemberSubscribeEvent',
|
|
memberId: newMember.id,
|
|
asserts: [
|
|
{
|
|
subscribed: true,
|
|
source: 'admin',
|
|
newsletter_id: newsletters[0].id,
|
|
created_at: before
|
|
}, {
|
|
subscribed: true,
|
|
source: 'admin',
|
|
newsletter_id: newsletters[1].id,
|
|
created_at: after
|
|
}, {
|
|
subscribed: false,
|
|
source: 'admin',
|
|
newsletter_id: newsletters[0].id,
|
|
created_at: after
|
|
}
|
|
]
|
|
});
|
|
|
|
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({
|
|
'content-version': anyContentVersion,
|
|
etag: anyEtag
|
|
});
|
|
|
|
const events = eventsBody.events;
|
|
|
|
// The order will be different in each test because two newsletter_events have the same created_at timestamp. And events are ordered by created_at desc, id desc (id will be different each time).
|
|
matchArrayWithoutOrder(events, [
|
|
{
|
|
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: 'signup_event'
|
|
},
|
|
{
|
|
type: 'newsletter_event',
|
|
data: {
|
|
subscribed: true,
|
|
source: 'admin',
|
|
newsletter_id: newsletters[0].id,
|
|
newsletter: {
|
|
id: newsletters[0].id
|
|
}
|
|
}
|
|
}
|
|
]);
|
|
|
|
clock.restore();
|
|
});
|
|
|
|
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(buildMemberMatcherShallowIncludesWithTiers(0, 2))
|
|
})
|
|
.matchHeaderSnapshot({
|
|
'content-version': anyContentVersion,
|
|
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
|
|
};
|
|
})
|
|
});
|
|
});
|
|
|
|
it('Can add a subscription', async function () {
|
|
const memberId = testUtils.DataGenerator.Content.members[0].id;
|
|
|
|
// Get the stripe price ID of the default price for month
|
|
const price = await stripeMocker.getPriceForTier('default-product', 'month');
|
|
|
|
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: [subscriptionSnapshotWithTier],
|
|
newsletters: new Array(1).fill(newsletterSnapshot),
|
|
tiers: [tierSnapshot]
|
|
})
|
|
})
|
|
.matchHeaderSnapshot({
|
|
'content-version': anyContentVersion,
|
|
etag: anyEtag
|
|
});
|
|
|
|
// 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: [subscriptionSnapshotWithTier],
|
|
newsletters: new Array(1).fill(newsletterSnapshot),
|
|
tiers: [tierSnapshot]
|
|
})
|
|
})
|
|
.matchHeaderSnapshot({
|
|
'content-version': anyContentVersion,
|
|
etag: anyEtag
|
|
});
|
|
});
|
|
|
|
// Delete a member
|
|
|
|
it('Can destroy', async function () {
|
|
const member = {
|
|
name: 'test',
|
|
email: 'memberTestDestroy@test.com'
|
|
};
|
|
|
|
const {body} = await agent
|
|
.post(`/members/`)
|
|
.body({members: [member]})
|
|
.expectStatus(201)
|
|
.matchBodySnapshot({
|
|
members: new Array(1).fill(buildMemberMatcherShallowIncludesWithTiers(0, 2))
|
|
})
|
|
.matchHeaderSnapshot({
|
|
'content-version': anyContentVersion,
|
|
etag: anyEtag,
|
|
location: anyLocationFor('members')
|
|
});
|
|
|
|
const newMember = body.members[0];
|
|
|
|
await agent
|
|
.delete(`/members/${newMember.id}`)
|
|
.expectStatus(204)
|
|
.expectEmptyBody()
|
|
.matchHeaderSnapshot({
|
|
'content-version': anyContentVersion,
|
|
etag: anyEtag
|
|
});
|
|
|
|
await agent
|
|
.get(`/members/${newMember.id}/`)
|
|
.expectStatus(404)
|
|
.matchBodySnapshot({
|
|
errors: [{
|
|
id: anyUuid
|
|
}]
|
|
})
|
|
.matchHeaderSnapshot({
|
|
'content-version': anyContentVersion,
|
|
etag: anyEtag
|
|
});
|
|
});
|
|
|
|
it('Cannot delete a non-existent member', async function () {
|
|
await agent
|
|
.delete('/members/abcd1234abcd1234abcd1234')
|
|
.expectStatus(404)
|
|
.matchBodySnapshot({
|
|
errors: [{
|
|
id: anyUuid
|
|
}]
|
|
})
|
|
.matchHeaderSnapshot({
|
|
'content-version': anyContentVersion,
|
|
etag: anyEtag
|
|
});
|
|
});
|
|
|
|
// Export members to CSV
|
|
|
|
it('Can export CSV', async function () {
|
|
const res = await agent
|
|
.get(`/members/upload/?limit=all`)
|
|
.expectStatus(200)
|
|
.expectEmptyBody() // express-test body parsing doesn't support CSV
|
|
.matchHeaderSnapshot({
|
|
etag: anyEtag,
|
|
'content-version': anyContentVersion,
|
|
'content-length': anyContentLength,
|
|
'content-disposition': anyString
|
|
});
|
|
|
|
res.text.should.match(/id,email,name,note,subscribed_to_emails,complimentary_plan,stripe_customer_id,created_at,deleted_at,labels,tiers/);
|
|
|
|
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'));
|
|
should.exist(csv.data.find(row => row.tiers.length > 0));
|
|
should.exist(csv.data.find(row => row.labels.length > 0));
|
|
});
|
|
|
|
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-version': anyContentVersion,
|
|
'content-disposition': anyString
|
|
});
|
|
|
|
res.text.should.match(/id,email,name,note,subscribed_to_emails,complimentary_plan,stripe_customer_id,created_at,deleted_at,labels,tiers/);
|
|
|
|
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'));
|
|
// note that this member doesn't have tiers
|
|
should.exist(csv.data.find(row => row.labels.length > 0));
|
|
});
|
|
|
|
it('Can delete a member without cancelling Stripe Subscription', async function () {
|
|
let subscriptionCanceled = false;
|
|
mockManager.restore();
|
|
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({
|
|
'content-version': anyContentVersion,
|
|
etag: anyEtag
|
|
});
|
|
|
|
assert.equal(subscriptionCanceled, false, 'expected subscription not to be canceled');
|
|
});
|
|
|
|
it('Can delete a member while cancelling Stripe Subscription', async function () {
|
|
let subscriptionCanceled = false;
|
|
mockManager.restore();
|
|
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', 3);
|
|
|
|
await agent
|
|
.delete(`members/${memberToDelete.id}/?cancel=true`)
|
|
.expectStatus(204)
|
|
.expectEmptyBody()
|
|
.matchHeaderSnapshot({
|
|
'content-version': anyContentVersion,
|
|
etag: anyEtag
|
|
});
|
|
|
|
assert.equal(subscriptionCanceled, true, 'expected subscription to be canceled');
|
|
});
|
|
|
|
// Get stats
|
|
|
|
it('Can fetch member counts stats', async function () {
|
|
await agent
|
|
.get(`/members/stats/count/`)
|
|
.expectStatus(200)
|
|
.matchBodySnapshot({
|
|
data: [{
|
|
date: anyISODate
|
|
}]
|
|
})
|
|
.matchHeaderSnapshot({
|
|
'content-version': anyContentVersion,
|
|
etag: anyEtag
|
|
});
|
|
});
|
|
|
|
it('Errors when fetching stats with unknown days param value', async function () {
|
|
sinon.stub(logging, 'error');
|
|
await agent
|
|
.get('members/stats/?days=nope')
|
|
.expectStatus(422)
|
|
.matchHeaderSnapshot({
|
|
'content-version': anyContentVersion,
|
|
etag: anyEtag
|
|
})
|
|
.matchBodySnapshot({
|
|
errors: [{
|
|
id: anyErrorId
|
|
}]
|
|
});
|
|
});
|
|
|
|
it('Can filter on newsletter slug', async function () {
|
|
await agent
|
|
.get('/members/?filter=newsletters:weekly-newsletter')
|
|
.expectStatus(200)
|
|
.matchBodySnapshot({
|
|
members: [
|
|
buildMemberMatcherShallowIncludesWithTiers(undefined, 2),
|
|
buildMemberMatcherShallowIncludesWithTiers(undefined, 1),
|
|
buildMemberMatcherShallowIncludesWithTiers(undefined, 2)
|
|
]
|
|
})
|
|
.matchHeaderSnapshot({
|
|
'content-version': anyContentVersion,
|
|
etag: anyEtag
|
|
});
|
|
});
|
|
|
|
it('Can filter on tier slug', async function () {
|
|
await agent
|
|
.get('/members/?include=tiers&filter=tier:default-product')
|
|
.expectStatus(200)
|
|
.matchBodySnapshot({
|
|
members: [
|
|
buildMemberMatcherShallowIncludesWithTiers(1, 1),
|
|
buildMemberMatcherShallowIncludesWithTiers(1, 1),
|
|
buildMemberMatcherShallowIncludesWithTiers(1, 1),
|
|
buildMemberMatcherShallowIncludesWithTiers(1, 1),
|
|
buildMemberMatcherShallowIncludesWithTiers(1, 1),
|
|
buildMemberMatcherShallowIncludesWithTiers(1, 0),
|
|
buildMemberMatcherShallowIncludesWithTiers(1, 2),
|
|
buildMemberMatcherShallowIncludesWithTiers(1, 1)
|
|
]
|
|
})
|
|
.matchHeaderSnapshot({
|
|
'content-version': anyContentVersion,
|
|
etag: anyEtag
|
|
});
|
|
});
|
|
|
|
// Edit a member
|
|
it('Can add and edit with custom newsletters', async function () {
|
|
// Add custom newsletter list to new member
|
|
const member = {
|
|
name: 'test newsletter',
|
|
email: 'memberTestAddNewsletter2@test.com',
|
|
note: 'test note',
|
|
subscribed: false,
|
|
labels: ['test-label'],
|
|
newsletters: [{id: testUtils.DataGenerator.Content.newsletters[1].id}]
|
|
};
|
|
|
|
const {body} = await agent
|
|
.post(`/members/`)
|
|
.body({members: [member]})
|
|
.expectStatus(201)
|
|
.matchBodySnapshot({
|
|
members: [{
|
|
id: anyObjectId,
|
|
uuid: anyUuid,
|
|
created_at: anyISODateTime,
|
|
updated_at: anyISODateTime,
|
|
subscriptions: anyArray,
|
|
labels: anyArray,
|
|
newsletters: Array(1).fill(newsletterSnapshot)
|
|
}]
|
|
})
|
|
.matchHeaderSnapshot({
|
|
'content-version': anyContentVersion,
|
|
etag: anyEtag,
|
|
location: anyLocationFor('members')
|
|
});
|
|
|
|
const memberId = body.members[0].id;
|
|
const editedMember = {
|
|
newsletters: [{id: testUtils.DataGenerator.Content.newsletters[0].id}]
|
|
};
|
|
|
|
// Edit newsletter list for member
|
|
await agent
|
|
.put(`/members/${memberId}`)
|
|
.body({members: [editedMember]})
|
|
.expectStatus(200)
|
|
.matchBodySnapshot({
|
|
members: [{
|
|
id: anyObjectId,
|
|
uuid: anyUuid,
|
|
created_at: anyISODateTime,
|
|
updated_at: anyISODateTime,
|
|
subscriptions: anyArray,
|
|
labels: anyArray,
|
|
newsletters: Array(1).fill(newsletterSnapshot)
|
|
}]
|
|
})
|
|
.matchHeaderSnapshot({
|
|
'content-version': anyContentVersion,
|
|
etag: anyEtag
|
|
});
|
|
|
|
sinon.stub(logging, 'error');
|
|
await agent
|
|
.post(`/members/`)
|
|
.body({members: [member]})
|
|
.expectStatus(422);
|
|
});
|
|
|
|
it('Setting subscribed when editing a member won\'t reset to default newsletters', async function () {
|
|
// First check that this newsletter is off by default, or this test would not make sense
|
|
const newsletter = await models.Newsletter.findOne({id: testUtils.DataGenerator.Content.newsletters[0].id}, {require: true});
|
|
assert.equal(newsletter.get('subscribe_on_signup'), false, 'This test expects the newsletter to be off by default');
|
|
|
|
// Add custom newsletter list to new member
|
|
const member = {
|
|
name: 'test newsletter',
|
|
email: 'memberTestChangeSubscribedAttribute@test.com',
|
|
newsletters: [
|
|
{
|
|
id: testUtils.DataGenerator.Content.newsletters[0].id // This is off by default
|
|
},
|
|
{
|
|
id: testUtils.DataGenerator.Content.newsletters[1].id
|
|
}
|
|
]
|
|
};
|
|
|
|
const {body} = await agent
|
|
.post(`/members/`)
|
|
.body({members: [member]})
|
|
.expectStatus(201)
|
|
.matchBodySnapshot({
|
|
members: [{
|
|
id: anyObjectId,
|
|
uuid: anyUuid,
|
|
created_at: anyISODateTime,
|
|
updated_at: anyISODateTime,
|
|
subscriptions: anyArray,
|
|
labels: anyArray,
|
|
newsletters: Array(2).fill(newsletterSnapshot)
|
|
}]
|
|
})
|
|
.matchHeaderSnapshot({
|
|
'content-version': anyContentVersion,
|
|
etag: anyEtag,
|
|
location: anyLocationFor('members')
|
|
});
|
|
|
|
const memberId = body.members[0].id;
|
|
|
|
const editedMember = {
|
|
subscribed: true // no change
|
|
};
|
|
|
|
// Edit member
|
|
const {body: body2} = await agent
|
|
.put(`/members/${memberId}`)
|
|
.body({members: [editedMember]})
|
|
.expectStatus(200)
|
|
.matchBodySnapshot({
|
|
members: [{
|
|
id: anyObjectId,
|
|
uuid: anyUuid,
|
|
created_at: anyISODateTime,
|
|
updated_at: anyISODateTime,
|
|
subscriptions: anyArray,
|
|
labels: anyArray,
|
|
newsletters: Array(2).fill(newsletterSnapshot)
|
|
}]
|
|
})
|
|
.matchHeaderSnapshot({
|
|
'content-version': anyContentVersion,
|
|
etag: anyEtag
|
|
});
|
|
const changedMember = body2.members[0];
|
|
assert.equal(changedMember.newsletters.length, 2);
|
|
assert.ok(changedMember.newsletters.find(n => n.id === testUtils.DataGenerator.Content.newsletters[0].id), 'The member is still subscribed for a newsletter that is off by default');
|
|
assert.ok(changedMember.newsletters.find(n => n.id === testUtils.DataGenerator.Content.newsletters[1].id), 'The member is still subscribed for the newsletter it subscribed to');
|
|
});
|
|
|
|
it('Adding newsletters to member with no subscriptions works even with subscribed false', async function () {
|
|
// Add member with no subscriptions
|
|
const member = {
|
|
name: 'test newsletter',
|
|
email: 'memberAddNewsletterSubscribed@test.com',
|
|
newsletters: []
|
|
};
|
|
|
|
const {body} = await agent
|
|
.post(`/members/`)
|
|
.body({members: [member]})
|
|
.expectStatus(201)
|
|
.matchBodySnapshot({
|
|
members: [{
|
|
id: anyObjectId,
|
|
uuid: anyUuid,
|
|
created_at: anyISODateTime,
|
|
updated_at: anyISODateTime,
|
|
subscriptions: anyArray,
|
|
labels: anyArray,
|
|
newsletters: Array(0).fill(newsletterSnapshot)
|
|
}]
|
|
})
|
|
.matchHeaderSnapshot({
|
|
'content-version': anyContentVersion,
|
|
etag: anyEtag,
|
|
location: anyLocationFor('members')
|
|
});
|
|
|
|
const memberId = body.members[0].id;
|
|
|
|
const editedMember = {
|
|
subscribed: false,
|
|
newsletters: [
|
|
{
|
|
id: testUtils.DataGenerator.Content.newsletters[0].id
|
|
}
|
|
]
|
|
};
|
|
|
|
// Edit member
|
|
const {body: body2} = await agent
|
|
.put(`/members/${memberId}`)
|
|
.body({members: [editedMember]})
|
|
.expectStatus(200)
|
|
.matchBodySnapshot({
|
|
members: [{
|
|
id: anyObjectId,
|
|
uuid: anyUuid,
|
|
created_at: anyISODateTime,
|
|
updated_at: anyISODateTime,
|
|
subscriptions: anyArray,
|
|
labels: anyArray,
|
|
newsletters: Array(1).fill(newsletterSnapshot)
|
|
}]
|
|
})
|
|
.matchHeaderSnapshot({
|
|
'content-version': anyContentVersion,
|
|
etag: anyEtag
|
|
});
|
|
const changedMember = body2.members[0];
|
|
assert.equal(changedMember.newsletters.length, 1);
|
|
assert.ok(changedMember.newsletters.find(n => n.id === testUtils.DataGenerator.Content.newsletters[0].id), 'The member should be subscribed to the newsletter');
|
|
});
|
|
|
|
it('Updating member data without newsletters does not change newsletters', async function () {
|
|
// check that this newsletter is archived, or this test would not make sense
|
|
const archivedNewsletterId = testUtils.DataGenerator.Content.newsletters[2].id;
|
|
const archivedNewsletter = await models.Newsletter.findOne({id: archivedNewsletterId}, {require: true});
|
|
assert.equal(archivedNewsletter.get('status'), 'archived', 'This test expects the newsletter to be archived');
|
|
|
|
const member = await models.Member.findOne({id: testUtils.DataGenerator.Content.members[5].id}, {withRelated: ['newsletters']});
|
|
const memberNewsletters = member.related('newsletters').models;
|
|
// NOTE: removed this call for now; it's not necessary as it's just 'bonus validation' before executing the api calls
|
|
// unfortunately it led to some issues where the object id was not in sync between fixture data and the db (unsure of cause)
|
|
// assert.equal(memberNewsletters[1].id, archivedNewsletterId, 'This test expects the member to be subscribed to an archived newsletter');
|
|
assert.equal(memberNewsletters.length, 2, 'This test expects the member to have two newsletter subscriptions');
|
|
|
|
const memberId = member.get('id');
|
|
const editedMember = {
|
|
id: memberId,
|
|
name: 'new name'
|
|
};
|
|
|
|
// edit member
|
|
const {body} = await agent
|
|
.put(`/members/${memberId}`)
|
|
.body({members: [editedMember]})
|
|
.expectStatus(200)
|
|
.matchBodySnapshot({
|
|
members: [{
|
|
id: anyObjectId,
|
|
uuid: anyUuid,
|
|
created_at: anyISODateTime,
|
|
updated_at: anyISODateTime,
|
|
subscriptions: anyArray,
|
|
labels: anyArray,
|
|
newsletters: Array(1).fill(newsletterSnapshot)
|
|
}]
|
|
})
|
|
.matchHeaderSnapshot({
|
|
'content-version': anyContentVersion,
|
|
etag: anyEtag
|
|
});
|
|
|
|
const changedMember = body.members[0];
|
|
assert.equal(changedMember.newsletters.length, 1); // the api only returns active newsletters
|
|
assert.ok(changedMember.newsletters.find(n => n.id === testUtils.DataGenerator.Content.newsletters[1].id), 'The member is still subscribed to an active newsletter');
|
|
|
|
const changedMemberFromDb = await models.Member.findOne({id: testUtils.DataGenerator.Content.members[5].id}, {withRelated: ['newsletters']});
|
|
assert.ok(changedMemberFromDb.related('newsletters').models.find(n => n.id === testUtils.DataGenerator.Content.newsletters[2].id), 'The member is still subscribed to the archived newsletter it subscribed to');
|
|
});
|
|
|
|
it('Updating newsletter subscriptions does not unsubscribe member from archived newsletter', async function () {
|
|
// check that this newsletter is archived, or this test would not make sense
|
|
const archivedNewsletterId = testUtils.DataGenerator.Content.newsletters[2].id;
|
|
const archivedNewsletter = await models.Newsletter.findOne({id: archivedNewsletterId}, {require: true});
|
|
assert.equal(archivedNewsletter.get('status'), 'archived', 'This test expects the newsletter to be archived');
|
|
|
|
const member = await models.Member.findOne({id: testUtils.DataGenerator.Content.members[5].id}, {withRelated: ['newsletters']});
|
|
const memberNewsletters = member.related('newsletters').models;
|
|
// NOTE: removed this call for now; it's not necessary as it's just 'bonus validation' before executing the api calls
|
|
// unfortunately it led to some issues where the object id was not in sync between fixture data and the db (unsure of cause)
|
|
// assert.equal(memberNewsletters[1].id, archivedNewsletterId, 'This test expects the member to be subscribed to an archived newsletter');
|
|
assert.equal(memberNewsletters.length, 2, 'This test expects the member to have two newsletter subscriptions');
|
|
|
|
// remove active newsletter subscriptions
|
|
const memberId = member.get('id');
|
|
const editedMember = {
|
|
newsletters: []
|
|
};
|
|
|
|
// edit member
|
|
const {body} = await agent
|
|
.put(`/members/${memberId}`)
|
|
.body({members: [editedMember]})
|
|
.expectStatus(200)
|
|
.matchBodySnapshot({
|
|
members: [{
|
|
id: anyObjectId,
|
|
uuid: anyUuid,
|
|
created_at: anyISODateTime,
|
|
updated_at: anyISODateTime,
|
|
subscriptions: anyArray,
|
|
labels: anyArray,
|
|
newsletters: new Array(0)
|
|
}]
|
|
})
|
|
.matchHeaderSnapshot({
|
|
'content-version': anyContentVersion,
|
|
etag: anyEtag
|
|
});
|
|
|
|
const changedMember = body.members[0];
|
|
assert.equal(changedMember.newsletters.length, 0); // the api only returns active newsletters, so this member should have none
|
|
|
|
const changedMemberFromDb = await models.Member.findOne({id: testUtils.DataGenerator.Content.members[5].id}, {withRelated: ['newsletters']});
|
|
assert.ok(changedMemberFromDb.related('newsletters').models.find(n => n.id === testUtils.DataGenerator.Content.newsletters[2].id), 'The member is still subscribed to the archived newsletter it subscribed to');
|
|
});
|
|
|
|
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({
|
|
'content-version': anyContentVersion,
|
|
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({
|
|
'content-version': anyContentVersion,
|
|
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({
|
|
'content-version': anyContentVersion,
|
|
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({
|
|
'content-version': anyContentVersion,
|
|
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({
|
|
'content-version': anyContentVersion,
|
|
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({
|
|
'content-version': anyContentVersion,
|
|
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({
|
|
'content-version': anyContentVersion,
|
|
etag: anyEtag
|
|
});
|
|
|
|
await assertMemberEvents({
|
|
eventType: 'MemberSubscribeEvent',
|
|
memberId: newMember.id,
|
|
asserts: filteredNewsletters.map((n) => {
|
|
return {
|
|
subscribed: true,
|
|
source: 'admin',
|
|
newsletter_id: n.id
|
|
};
|
|
})
|
|
});
|
|
});
|
|
});
|
|
|
|
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({
|
|
'content-version': anyContentVersion,
|
|
etag: anyEtag
|
|
});
|
|
|
|
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({
|
|
'content-version': anyContentVersion,
|
|
etag: anyEtag
|
|
});
|
|
});
|
|
|
|
it('Can bulk unsubscribe members from specific newsletter', async function () {
|
|
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/?all=true`)
|
|
.body({bulk: {
|
|
action: 'unsubscribe',
|
|
newsletter: model.relations.newsletters.models[0].id,
|
|
meta: {}
|
|
}})
|
|
.expectStatus(200)
|
|
.matchBodySnapshot({
|
|
bulk: {
|
|
meta: {
|
|
stats: {
|
|
successful: 4,
|
|
unsuccessful: 0
|
|
},
|
|
unsuccessfulData: [],
|
|
errors: []
|
|
}
|
|
}
|
|
});
|
|
const updatedModel = await models.Member.findOne({id: member.id}, {withRelated: 'newsletters'});
|
|
// ensure they were unsubscribed from the single 'chosen' newsletter
|
|
should(updatedModel.relations.newsletters.models.length).equal(newsletterCount - 1);
|
|
});
|
|
|
|
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({
|
|
'content-version': anyContentVersion,
|
|
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({
|
|
'content-version': anyContentVersion,
|
|
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({
|
|
'content-version': anyContentVersion,
|
|
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({
|
|
'content-version': anyContentVersion,
|
|
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({
|
|
'content-version': anyContentVersion,
|
|
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({
|
|
'content-version': anyContentVersion,
|
|
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]);
|
|
});
|
|
|
|
it('Can bulk delete members', async function () {
|
|
await agent
|
|
.delete('/members?all=true')
|
|
.expectStatus(200)
|
|
.matchBodySnapshot({
|
|
meta: {
|
|
stats: {
|
|
successful: 8,
|
|
unsuccessful: 0
|
|
},
|
|
unsuccessfulIds: [],
|
|
errors: []
|
|
}
|
|
});
|
|
});
|
|
});
|