mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-24 03:14:03 +03:00
756f86dbdc
refs https://github.com/TryGhost/Team/issues/1561 With multiple newsletters, unsubscribe links will also need to have a unique reference to the newsletter that the email is for, so that we can unsubscribe members from the particular newsletter automatically when they click on the link. As our existing pattern for members is to use UUID as the external unique reference, this change adds UUID to newsletter schema and populates the existing newsletters with a UUID value. - adds new `uuid` column to newsletter schema - updates newsletter model to add default uuid - updates default newsletter migration to add `uuid` - drops nullable on `uuid` column later in migrations once we have populated existing newsletters
1629 lines
51 KiB
JavaScript
1629 lines
51 KiB
JavaScript
const {agentProvider, mockManager, fixtureManager, matchers} = require('../../utils/e2e-framework');
|
|
const {anyEtag, anyObjectId, anyUuid, anyISODateTime, anyISODate, anyString, anyArray, anyLocationFor, anyErrorId} = matchers;
|
|
|
|
const assert = require('assert');
|
|
const nock = require('nock');
|
|
const should = require('should');
|
|
const sinon = require('sinon');
|
|
const testUtils = require('../../utils');
|
|
const Papa = require('papaparse');
|
|
|
|
const models = require('../../../core/server/models');
|
|
|
|
async function assertMemberEvents({eventType, memberId, asserts}) {
|
|
const events = await models[eventType].where('member_id', memberId).fetchAll();
|
|
events.map(e => e.toJSON()).should.match(asserts);
|
|
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 getNewsletters() {
|
|
return (await models.Newsletter.findAll({filter: 'status:active'})).models;
|
|
}
|
|
|
|
const newsletterSnapshot = {
|
|
id: anyObjectId,
|
|
uuid: anyUuid,
|
|
created_at: anyISODateTime,
|
|
updated_at: anyISODateTime
|
|
};
|
|
|
|
function buildMemberWithoutIncludesSnapshot(options) {
|
|
return {
|
|
id: anyObjectId,
|
|
uuid: anyUuid,
|
|
created_at: anyISODateTime,
|
|
updated_at: anyISODateTime,
|
|
newsletters: new Array(options.newsletters).fill(newsletterSnapshot)
|
|
};
|
|
}
|
|
|
|
const memberMatcherShallowIncludes = {
|
|
id: anyObjectId,
|
|
uuid: anyUuid,
|
|
created_at: anyISODateTime,
|
|
updated_at: anyISODateTime,
|
|
subscriptions: anyArray,
|
|
labels: anyArray,
|
|
newsletters: anyArray
|
|
};
|
|
|
|
let agent;
|
|
|
|
describe('Members API without Stripe', function () {
|
|
before(async function () {
|
|
agent = await agentProvider.getAdminAPIAgent();
|
|
await fixtureManager.init();
|
|
await agent.loginAsOwner();
|
|
});
|
|
|
|
before(async function () {
|
|
await agent
|
|
.delete('/settings/stripe/connect/')
|
|
.expectStatus(200);
|
|
});
|
|
|
|
beforeEach(function () {
|
|
mockManager.mockMail();
|
|
});
|
|
|
|
afterEach(function () {
|
|
mockManager.restore();
|
|
});
|
|
|
|
it('Add should fail when comped flag is passed in but Stripe is not enabled', async function () {
|
|
const newMember = {
|
|
email: 'memberTestAdd@test.com',
|
|
comped: true
|
|
};
|
|
|
|
await agent
|
|
.post(`members/`)
|
|
.body({members: [newMember]})
|
|
.expectStatus(422)
|
|
.matchHeaderSnapshot({
|
|
etag: anyEtag
|
|
})
|
|
.matchBodySnapshot({
|
|
errors: [{
|
|
id: anyErrorId
|
|
}]
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Members API', function () {
|
|
let newsletters;
|
|
|
|
before(async function () {
|
|
agent = await agentProvider.getAdminAPIAgent();
|
|
await fixtureManager.init('newsletters', 'members:newsletters');
|
|
await agent.loginAsOwner();
|
|
|
|
newsletters = await getNewsletters();
|
|
});
|
|
|
|
beforeEach(function () {
|
|
mockManager.mockLabsEnabled('multipleProducts');
|
|
mockManager.mockLabsEnabled('multipleNewsletters');
|
|
mockManager.mockStripe();
|
|
mockManager.mockMail();
|
|
});
|
|
|
|
afterEach(function () {
|
|
mockManager.restore();
|
|
});
|
|
|
|
// List Members
|
|
|
|
it('Can browse', async function () {
|
|
await agent
|
|
.get('/members/')
|
|
.expectStatus(200)
|
|
.matchBodySnapshot({
|
|
members: new Array(8).fill(memberMatcherShallowIncludes)
|
|
})
|
|
.matchHeaderSnapshot({
|
|
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(memberMatcherShallowIncludes)
|
|
})
|
|
.matchHeaderSnapshot({
|
|
etag: anyEtag
|
|
});
|
|
});
|
|
|
|
it('Can browse with search', async function () {
|
|
await agent
|
|
.get('/members/?search=member1')
|
|
.expectStatus(200)
|
|
.matchBodySnapshot({
|
|
members: new Array(1).fill(memberMatcherShallowIncludes)
|
|
})
|
|
.matchHeaderSnapshot({
|
|
etag: anyEtag
|
|
});
|
|
});
|
|
|
|
it('Can filter by paid status', async function () {
|
|
await agent
|
|
.get('/members/?filter=status:paid')
|
|
.expectStatus(200)
|
|
.matchBodySnapshot({
|
|
members: new Array(5).fill(memberMatcherShallowIncludes)
|
|
})
|
|
.matchHeaderSnapshot({
|
|
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(memberMatcherShallowIncludes)
|
|
})
|
|
.matchHeaderSnapshot({
|
|
etag: anyEtag
|
|
});
|
|
});
|
|
|
|
it('Can ignore any unknown includes', async function () {
|
|
await agent
|
|
.get('/members/?filter=status:paid&include=emailRecipients')
|
|
.expectStatus(200)
|
|
.matchBodySnapshot({
|
|
members: new Array(5).fill(memberMatcherShallowIncludes)
|
|
})
|
|
.matchHeaderSnapshot({
|
|
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': anyString
|
|
})
|
|
.matchBodySnapshot({
|
|
members: new Array(8).fill(memberMatcherShallowIncludes)
|
|
})
|
|
.expect(({body}) => {
|
|
const {members} = body;
|
|
assert.equal(members[0].email_open_rate > members[1].email_open_rate, true, 'Expected the first member to have a greater open rate than the second.');
|
|
});
|
|
|
|
await agent
|
|
.get('members/?order=email_open_rate%20asc')
|
|
.expectStatus(200)
|
|
.matchHeaderSnapshot({
|
|
etag: anyEtag,
|
|
'content-length': anyString
|
|
})
|
|
.matchBodySnapshot({
|
|
members: new Array(8).fill(memberMatcherShallowIncludes)
|
|
})
|
|
.expect(({body}) => {
|
|
const {members} = body;
|
|
assert.equal(members[0].email_open_rate < members[1].email_open_rate, true, 'Expected the first member to have a 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: [memberMatcherShallowIncludes]
|
|
})
|
|
.matchHeaderSnapshot({
|
|
etag: anyEtag
|
|
});
|
|
});
|
|
|
|
it('Search by case-insensitive email MEMBER2 receives member with email member2@test.com', async function () {
|
|
await agent
|
|
.get('members/?search=MEMBER2')
|
|
.expectStatus(200)
|
|
.matchBodySnapshot({
|
|
members: [memberMatcherShallowIncludes]
|
|
})
|
|
.matchHeaderSnapshot({
|
|
etag: anyEtag
|
|
});
|
|
});
|
|
|
|
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: [memberMatcherShallowIncludes]
|
|
})
|
|
.matchHeaderSnapshot({
|
|
etag: anyEtag
|
|
});
|
|
});
|
|
|
|
it('Search for non existing member returns empty result set', async function () {
|
|
await agent
|
|
.get('members/?search=do_not_exist')
|
|
.expectStatus(200)
|
|
.matchHeaderSnapshot({
|
|
etag: anyEtag
|
|
})
|
|
.matchBodySnapshot({
|
|
members: []
|
|
});
|
|
});
|
|
|
|
// Read a member
|
|
|
|
it('Can read', async function () {
|
|
await agent
|
|
.get(`/members/${testUtils.DataGenerator.Content.members[0].id}/`)
|
|
.expectStatus(200)
|
|
.matchBodySnapshot({
|
|
members: new Array(1).fill(memberMatcherShallowIncludes)
|
|
})
|
|
.matchHeaderSnapshot({
|
|
etag: anyEtag
|
|
});
|
|
});
|
|
|
|
it('Can read and include email_recipients', async function () {
|
|
await agent
|
|
.get(`/members/${testUtils.DataGenerator.Content.members[0].id}/?include=email_recipients`)
|
|
.expectStatus(200)
|
|
.matchBodySnapshot({
|
|
members: new Array(1).fill(memberMatcherShallowIncludes)
|
|
})
|
|
.matchHeaderSnapshot({
|
|
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(memberMatcherShallowIncludes)
|
|
})
|
|
.matchHeaderSnapshot({
|
|
etag: anyEtag,
|
|
location: anyLocationFor('members')
|
|
});
|
|
const newMember = body.members[0];
|
|
|
|
await agent
|
|
.post(`/members/`)
|
|
.body({members: [member]})
|
|
.expectStatus(422);
|
|
|
|
await assertMemberEvents({
|
|
eventType: 'MemberStatusEvent',
|
|
memberId: newMember.id,
|
|
asserts: [
|
|
{
|
|
from_status: null,
|
|
to_status: 'free'
|
|
}
|
|
]
|
|
});
|
|
});
|
|
|
|
/*it('Can add and send a signup confirmation email (old)', async function () {
|
|
const member = {
|
|
name: 'Send Me Confirmation',
|
|
email: 'member_getting_confirmation@test.com',
|
|
subscribed: true
|
|
};
|
|
|
|
const queryParams = {
|
|
send_email: true,
|
|
email_type: 'signup'
|
|
};
|
|
|
|
const {body} = await agent
|
|
.post('/members/?send_email=true&email_type=signup')
|
|
.body({members: [member]})
|
|
.expectStatus(201)
|
|
.matchBodySnapshot({
|
|
members: [news]
|
|
})
|
|
.matchHeaderSnapshot({
|
|
etag: anyEtag,
|
|
location: anyString
|
|
});
|
|
|
|
const newMember = body.members[0];
|
|
|
|
mockManager.assert.sentEmail({
|
|
subject: '🙌 Complete your sign up to Ghost!',
|
|
to: 'member_getting_confirmation@test.com'
|
|
});
|
|
|
|
await assertMemberEvents({
|
|
eventType: 'MemberStatusEvent',
|
|
memberId: newMember.id,
|
|
asserts: [
|
|
{
|
|
from_status: null,
|
|
to_status: 'free'
|
|
}
|
|
]
|
|
});
|
|
|
|
await assertMemberEvents({
|
|
eventType: 'MemberSubscribeEvent',
|
|
memberId: newMember.id,
|
|
asserts: [
|
|
{
|
|
subscribed: true,
|
|
source: 'admin'
|
|
}
|
|
]
|
|
});
|
|
|
|
// @TODO: do we really need to delete this member here?
|
|
await agent
|
|
.delete(`members/${body.members[0].id}/`)
|
|
.matchHeaderSnapshot({
|
|
etag: anyEtag
|
|
})
|
|
.expectStatus(204);
|
|
|
|
const events = await models.MemberSubscribeEvent.findAll();
|
|
assert.equal(events.models.length, 0, 'There should be no MemberSubscribeEvent remaining.');
|
|
});*/
|
|
|
|
it('Can add and send a signup confirmation email', async function () {
|
|
const member = {
|
|
name: 'Send Me Confirmation',
|
|
email: 'member_getting_confirmation@test.com',
|
|
newsletters: [
|
|
newsletters[0],
|
|
newsletters[1]
|
|
]
|
|
};
|
|
|
|
const queryParams = {
|
|
send_email: true,
|
|
email_type: 'signup'
|
|
};
|
|
|
|
const {body} = await agent
|
|
.post('/members/?send_email=true&email_type=signup')
|
|
.body({members: [member]})
|
|
.expectStatus(201)
|
|
.matchBodySnapshot({
|
|
members: [
|
|
buildMemberWithoutIncludesSnapshot({
|
|
newsletters: 2
|
|
})
|
|
]
|
|
})
|
|
.matchHeaderSnapshot({
|
|
etag: anyEtag,
|
|
location: anyString
|
|
});
|
|
|
|
const newMember = body.members[0];
|
|
|
|
mockManager.assert.sentEmail({
|
|
subject: '🙌 Complete your sign up to Ghost!',
|
|
to: 'member_getting_confirmation@test.com'
|
|
});
|
|
|
|
await assertMemberEvents({
|
|
eventType: 'MemberStatusEvent',
|
|
memberId: newMember.id,
|
|
asserts: [
|
|
{
|
|
from_status: null,
|
|
to_status: 'free'
|
|
}
|
|
]
|
|
});
|
|
|
|
await assertMemberEvents({
|
|
eventType: 'MemberSubscribeEvent',
|
|
memberId: newMember.id,
|
|
asserts: [
|
|
{
|
|
subscribed: true,
|
|
source: 'admin',
|
|
newsletter_id: newsletters[0].id
|
|
},
|
|
{
|
|
subscribed: true,
|
|
source: 'admin',
|
|
newsletter_id: newsletters[1].id
|
|
}
|
|
]
|
|
});
|
|
|
|
// @TODO: do we really need to delete this member here?
|
|
await agent
|
|
.delete(`members/${body.members[0].id}/`)
|
|
.matchHeaderSnapshot({
|
|
etag: anyEtag
|
|
})
|
|
.expectStatus(204);
|
|
|
|
const events = await models.MemberSubscribeEvent.findAll();
|
|
assert.equal(events.models.length, 0, 'There should be no MemberSubscribeEvent remaining.');
|
|
});
|
|
|
|
it('Add should fail when passing incorrect email_type query parameter', async function () {
|
|
const newMember = {
|
|
name: 'test',
|
|
email: 'memberTestAdd@test.com'
|
|
};
|
|
|
|
await agent
|
|
.post(`members/?send_email=true&email_type=lel`)
|
|
.body({members: [newMember]})
|
|
.expectStatus(422)
|
|
.matchHeaderSnapshot({
|
|
etag: anyEtag
|
|
})
|
|
.matchBodySnapshot({
|
|
errors: [{
|
|
id: anyErrorId
|
|
}]
|
|
});
|
|
|
|
const statusEvents = await models.MemberStatusEvent.findAll();
|
|
assert.equal(statusEvents.models.length, 1, '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(memberMatcherShallowIncludes)
|
|
})
|
|
.matchHeaderSnapshot({
|
|
etag: anyEtag,
|
|
location: anyLocationFor('members')
|
|
});
|
|
|
|
const newMember = body.members[0];
|
|
|
|
const {body: body2} = await agent
|
|
.put(`/members/${newMember.id}/`)
|
|
.body({members: [compedPayload]})
|
|
.expectStatus(200)
|
|
.matchBodySnapshot({
|
|
members: new Array(1).fill(memberMatcherShallowIncludes)
|
|
})
|
|
.matchHeaderSnapshot({
|
|
etag: anyEtag
|
|
});
|
|
|
|
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
|
|
}]
|
|
});
|
|
});
|
|
|
|
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,
|
|
products: [
|
|
{
|
|
id: product.id
|
|
}
|
|
]
|
|
};
|
|
|
|
const {body: body2} = await agent
|
|
.put(`/members/${newMember.id}/`)
|
|
.body({members: [compedPayload]})
|
|
.expectStatus(200);
|
|
|
|
const updatedMember = body2.members[0];
|
|
assert.equal(updatedMember.status, 'comped', 'A comped member should have the comped status');
|
|
assert.equal(updatedMember.products.length, 1, 'The member should have one product');
|
|
|
|
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]],
|
|
products: [
|
|
{
|
|
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.products.length, 1, 'The member should have 1 product');
|
|
|
|
// Remove it
|
|
const removePayload = {
|
|
id: newMember.id,
|
|
email: newMember.email,
|
|
products: []
|
|
};
|
|
|
|
const {body: body2} = await agent
|
|
.put(`/members/${newMember.id}/`)
|
|
.body({members: [removePayload]})
|
|
.expectStatus(200);
|
|
|
|
const updatedMember = body2.members[0];
|
|
assert.equal(updatedMember.status, 'free', 'The member should have the free status');
|
|
assert.equal(updatedMember.products.length, 0, 'The member should have 0 products');
|
|
|
|
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 (complementary)', async function () {
|
|
const product = await getPaidProduct();
|
|
const initialMember = {
|
|
name: 'Name',
|
|
email: 'compedtest4@test.com',
|
|
subscribed: true,
|
|
newsletters: [newsletters[0]],
|
|
products: [
|
|
{
|
|
id: product.id
|
|
}
|
|
]
|
|
};
|
|
|
|
const {body} = await agent
|
|
.post(`/members/`)
|
|
.body({members: [initialMember]})
|
|
.expectStatus(201)
|
|
.matchBodySnapshot({
|
|
members: new Array(1).fill({
|
|
id: anyObjectId,
|
|
uuid: anyUuid,
|
|
created_at: anyISODateTime,
|
|
updated_at: anyISODateTime,
|
|
labels: anyArray,
|
|
subscriptions: anyArray,
|
|
products: new Array(1).fill({
|
|
id: anyObjectId,
|
|
monthly_price_id: anyObjectId,
|
|
yearly_price_id: anyObjectId,
|
|
created_at: anyISODateTime,
|
|
updated_at: anyISODateTime
|
|
}),
|
|
newsletters: new Array(1).fill(newsletterSnapshot)
|
|
})
|
|
})
|
|
.matchHeaderSnapshot({
|
|
etag: anyEtag,
|
|
location: anyLocationFor('members')
|
|
});
|
|
|
|
const newMember = body.members[0];
|
|
assert.equal(newMember.status, 'comped', 'The newly imported member should have the comped status');
|
|
|
|
await assertMemberEvents({
|
|
eventType: 'MemberStatusEvent',
|
|
memberId: newMember.id,
|
|
asserts: [{
|
|
from_status: null,
|
|
to_status: 'comped'
|
|
}]
|
|
});
|
|
|
|
await assertMemberEvents({
|
|
eventType: 'MemberSubscribeEvent',
|
|
memberId: newMember.id,
|
|
asserts: [{
|
|
subscribed: true,
|
|
source: 'admin'
|
|
}]
|
|
});
|
|
|
|
await assertMemberEvents({
|
|
eventType: 'MemberPaidSubscriptionEvent',
|
|
memberId: newMember.id,
|
|
asserts: []
|
|
});
|
|
});
|
|
|
|
it('Can create a member with an existing complimentary subscription', async function () {
|
|
const fakePrice = {
|
|
id: 'price_1',
|
|
product: '',
|
|
active: true,
|
|
nickname: 'Complimentary',
|
|
unit_amount: 0,
|
|
currency: 'usd',
|
|
type: 'recurring',
|
|
recurring: {
|
|
interval: 'year'
|
|
}
|
|
};
|
|
|
|
const fakeSubscription = {
|
|
id: 'sub_1',
|
|
customer: 'cus_1234',
|
|
status: 'active',
|
|
cancel_at_period_end: false,
|
|
metadata: {},
|
|
current_period_end: Date.now() / 1000 + 1000,
|
|
start_date: Date.now() / 1000,
|
|
plan: fakePrice,
|
|
items: {
|
|
data: [{
|
|
price: fakePrice
|
|
}]
|
|
}
|
|
};
|
|
|
|
const fakeCustomer = {
|
|
id: 'cus_1234',
|
|
name: 'Test Member',
|
|
email: 'create-member-comped-test@email.com',
|
|
subscriptions: {
|
|
type: 'list',
|
|
data: [fakeSubscription]
|
|
}
|
|
};
|
|
nock('https://api.stripe.com')
|
|
.persist()
|
|
.get(/v1\/.*/)
|
|
.reply((uri, body) => {
|
|
const [match, resource, id] = uri.match(/\/?v1\/(\w+)\/?(\w+)/) || [null];
|
|
|
|
if (!match) {
|
|
return [500];
|
|
}
|
|
|
|
if (resource === 'customers') {
|
|
return [200, fakeCustomer];
|
|
}
|
|
|
|
if (resource === 'subscriptions') {
|
|
return [200, fakeSubscription];
|
|
}
|
|
});
|
|
|
|
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,
|
|
products: anyArray,
|
|
newsletters: new Array(1).fill(newsletterSnapshot)
|
|
})
|
|
})
|
|
.matchHeaderSnapshot({
|
|
etag: anyEtag,
|
|
location: anyLocationFor('members')
|
|
});
|
|
|
|
const newMember = body.members[0];
|
|
assert.equal(newMember.status, 'comped', 'The created member should have the comped status');
|
|
|
|
await assertMemberEvents({
|
|
eventType: 'MemberStatusEvent',
|
|
memberId: newMember.id,
|
|
asserts: [
|
|
{
|
|
from_status: null,
|
|
to_status: 'free'
|
|
},
|
|
{
|
|
from_status: 'free',
|
|
to_status: 'comped'
|
|
}
|
|
]
|
|
});
|
|
|
|
await assertMemberEvents({
|
|
eventType: 'MemberSubscribeEvent',
|
|
memberId: newMember.id,
|
|
asserts: [
|
|
{
|
|
subscribed: true,
|
|
source: 'admin'
|
|
}
|
|
]
|
|
});
|
|
|
|
await assertMemberEvents({
|
|
eventType: 'MemberPaidSubscriptionEvent',
|
|
memberId: newMember.id,
|
|
asserts: [{
|
|
mrr_delta: 0
|
|
}]
|
|
});
|
|
});
|
|
|
|
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]
|
|
}
|
|
};
|
|
nock('https://api.stripe.com')
|
|
.persist()
|
|
.get(/v1\/.*/)
|
|
.reply((uri, body) => {
|
|
const [match, resource, id] = uri.match(/\/?v1\/(\w+)\/?(\w+)/) || [null];
|
|
|
|
if (!match) {
|
|
return [500];
|
|
}
|
|
|
|
if (resource === 'customers') {
|
|
return [200, fakeCustomer];
|
|
}
|
|
|
|
if (resource === 'subscriptions') {
|
|
return [200, fakeSubscription];
|
|
}
|
|
});
|
|
|
|
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,
|
|
products: anyArray,
|
|
newsletters: new Array(1).fill(newsletterSnapshot)
|
|
})
|
|
})
|
|
.matchHeaderSnapshot({
|
|
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 test
|
|
memberWithPaidSubscription = newMember;
|
|
});
|
|
|
|
it('Returns an identical member format for read, edit and browse', async function () {
|
|
if (!memberWithPaidSubscription) {
|
|
// Previous test failed
|
|
this.skip();
|
|
}
|
|
|
|
// Check status has been updated to 'free' after cancelling
|
|
const {body: readBody} = await agent.get('/members/' + memberWithPaidSubscription.id + '/');
|
|
assert.equal(readBody.members.length, 1, 'The member was not found in read');
|
|
const readMember = readBody.members[0];
|
|
|
|
// Note that we explicitly need to ask to include products while browsing
|
|
const {body: browseBody} = await agent.get(`/members/?search=${memberWithPaidSubscription.email}&include=products`);
|
|
assert.equal(browseBody.members.length, 1, 'The member was not found in browse');
|
|
const browseMember = browseBody.members[0];
|
|
|
|
// Check for this member with a paid subscription that the body results for the patch, get and browse endpoints are 100% identical
|
|
should.deepEqual(browseMember, readMember, 'Browsing a member returns a different format than reading a member');
|
|
should.deepEqual(memberWithPaidSubscription, readMember, 'Editing a member returns a different format than reading a member');
|
|
});
|
|
|
|
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(memberMatcherShallowIncludes)
|
|
})
|
|
.matchHeaderSnapshot({
|
|
etag: anyEtag,
|
|
location: anyLocationFor('members')
|
|
});
|
|
const newMember = body.members[0];
|
|
|
|
await assertMemberEvents({
|
|
eventType: 'MemberSubscribeEvent',
|
|
memberId: newMember.id,
|
|
asserts: [{
|
|
subscribed: true,
|
|
source: 'admin',
|
|
newsletter_id: newsletters[0].id
|
|
}]
|
|
});
|
|
await assertMemberEvents({
|
|
eventType: 'MemberStatusEvent',
|
|
memberId: newMember.id,
|
|
asserts: [{
|
|
from_status: null,
|
|
to_status: 'free'
|
|
}]
|
|
});
|
|
|
|
await agent
|
|
.put(`/members/${newMember.id}/`)
|
|
.body({members: [memberChanged]})
|
|
.expectStatus(200)
|
|
.matchBodySnapshot({
|
|
members: new Array(1).fill(memberMatcherShallowIncludes)
|
|
})
|
|
.matchHeaderSnapshot({
|
|
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
|
|
}
|
|
]
|
|
});
|
|
});
|
|
|
|
it('Can subscribe to a newsletter', async function () {
|
|
const memberToChange = {
|
|
name: 'change me',
|
|
email: 'member3change@test.com',
|
|
newsletters: [
|
|
newsletters[0]
|
|
]
|
|
};
|
|
|
|
const memberChanged = {
|
|
newsletters: [
|
|
newsletters[1]
|
|
]
|
|
};
|
|
|
|
const {body} = await agent
|
|
.post(`/members/`)
|
|
.body({members: [memberToChange]})
|
|
.expectStatus(201)
|
|
.matchBodySnapshot({
|
|
members: new Array(1).fill(memberMatcherShallowIncludes)
|
|
})
|
|
.matchHeaderSnapshot({
|
|
etag: anyEtag,
|
|
location: anyLocationFor('members')
|
|
});
|
|
const newMember = body.members[0];
|
|
|
|
await assertMemberEvents({
|
|
eventType: 'MemberSubscribeEvent',
|
|
memberId: newMember.id,
|
|
asserts: [{
|
|
subscribed: true,
|
|
source: 'admin',
|
|
newsletter_id: newsletters[0].id
|
|
}]
|
|
});
|
|
|
|
await agent
|
|
.put(`/members/${newMember.id}/`)
|
|
.body({members: [memberChanged]})
|
|
.expectStatus(200)
|
|
.matchBodySnapshot({
|
|
members: new Array(1).fill(memberMatcherShallowIncludes)
|
|
})
|
|
.matchHeaderSnapshot({
|
|
etag: anyEtag
|
|
});
|
|
|
|
await assertMemberEvents({
|
|
eventType: 'MemberSubscribeEvent',
|
|
memberId: newMember.id,
|
|
asserts: [
|
|
{
|
|
subscribed: true,
|
|
source: 'admin',
|
|
newsletter_id: newsletters[0].id
|
|
}, {
|
|
subscribed: true,
|
|
source: 'admin',
|
|
newsletter_id: newsletters[1].id
|
|
}, {
|
|
subscribed: false,
|
|
source: 'admin',
|
|
newsletter_id: newsletters[0].id
|
|
}
|
|
]
|
|
});
|
|
});
|
|
|
|
it('Subscribes to default newsletters', async function () {
|
|
const filtered = newsletters.filter(n => n.get('subscribe_on_signup'));
|
|
filtered.length.should.be.greaterThan(0, 'There should be at least one newsletter with subscribe on signup for this test to work');
|
|
|
|
const memberToCreate = {
|
|
name: 'create me',
|
|
email: 'member2create@test.com'
|
|
};
|
|
|
|
const {body} = await agent
|
|
.post(`/members/`)
|
|
.body({members: [memberToCreate]})
|
|
.expectStatus(201)
|
|
.matchBodySnapshot({
|
|
members: new Array(1).fill(memberMatcherShallowIncludes)
|
|
})
|
|
.matchHeaderSnapshot({
|
|
etag: anyEtag,
|
|
location: anyLocationFor('members')
|
|
});
|
|
|
|
const newMember = body.members[0];
|
|
newMember.newsletters.should.match([
|
|
{
|
|
id: filtered[0].id
|
|
},
|
|
{
|
|
id: filtered[1].id
|
|
}
|
|
]);
|
|
|
|
await assertMemberEvents({
|
|
eventType: 'MemberSubscribeEvent',
|
|
memberId: newMember.id,
|
|
asserts: filtered.map((n) => {
|
|
return {
|
|
subscribed: true,
|
|
source: 'admin',
|
|
newsletter_id: n.id
|
|
};
|
|
})
|
|
});
|
|
});
|
|
|
|
it('Can add a subscription', async function () {
|
|
const memberId = testUtils.DataGenerator.Content.members[0].id;
|
|
const price = testUtils.DataGenerator.Content.stripe_prices[0];
|
|
|
|
function nockCallback(method, uri, body) {
|
|
const [match, resource, id] = uri.match(/\/?v1\/(\w+)(?:\/(\w+))?/) || [null];
|
|
|
|
if (!match) {
|
|
return [500];
|
|
}
|
|
|
|
if (resource === 'customers') {
|
|
return [200, {id: 'cus_123', email: 'member1@test.com'}];
|
|
}
|
|
|
|
if (resource === 'subscriptions') {
|
|
const now = Math.floor(Date.now() / 1000);
|
|
return [200, {id: 'sub_123', customer: 'cus_123', cancel_at_period_end: false, items: {
|
|
data: [{price: {
|
|
id: price.stripe_price_id,
|
|
recurring: {
|
|
interval: price.interval
|
|
},
|
|
unit_amount: price.amount,
|
|
currency: price.currency.toLowerCase()
|
|
}}]
|
|
}, status: 'active', current_period_end: now + 24 * 3600, start_date: now}];
|
|
}
|
|
}
|
|
|
|
nock('https://api.stripe.com:443')
|
|
.persist()
|
|
.post(/v1\/.*/)
|
|
.reply((uri, body) => nockCallback('POST', uri, body));
|
|
|
|
nock('https://api.stripe.com:443')
|
|
.persist()
|
|
.get(/v1\/.*/)
|
|
.reply((uri, body) => nockCallback('GET', uri, body));
|
|
|
|
await agent
|
|
.post(`/members/${memberId}/subscriptions/`)
|
|
.body({
|
|
stripe_price_id: price.id
|
|
})
|
|
.expectStatus(200)
|
|
.matchBodySnapshot({
|
|
members: new Array(1).fill({
|
|
id: anyObjectId,
|
|
uuid: anyUuid,
|
|
created_at: anyISODateTime,
|
|
updated_at: anyISODateTime,
|
|
labels: anyArray,
|
|
subscriptions: [{
|
|
start_date: anyString,
|
|
current_period_end: anyString,
|
|
price: {
|
|
price_id: anyObjectId,
|
|
product: {
|
|
product_id: anyObjectId
|
|
}
|
|
}
|
|
}],
|
|
newsletters: anyArray
|
|
})
|
|
})
|
|
.matchHeaderSnapshot({
|
|
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: [{
|
|
start_date: anyString,
|
|
current_period_end: anyString,
|
|
price: {
|
|
price_id: anyObjectId,
|
|
product: {
|
|
product_id: anyObjectId
|
|
}
|
|
}
|
|
}],
|
|
newsletters: anyArray
|
|
})
|
|
})
|
|
.matchHeaderSnapshot({
|
|
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(memberMatcherShallowIncludes)
|
|
})
|
|
.matchHeaderSnapshot({
|
|
etag: anyEtag,
|
|
location: anyLocationFor('members')
|
|
});
|
|
|
|
const newMember = body.members[0];
|
|
|
|
await agent
|
|
.delete(`/members/${newMember.id}`)
|
|
.expectStatus(204)
|
|
.expectEmptyBody()
|
|
.matchHeaderSnapshot({
|
|
etag: anyEtag
|
|
});
|
|
|
|
await agent
|
|
.get(`/members/${newMember.id}/`)
|
|
.expectStatus(404)
|
|
.matchBodySnapshot({
|
|
errors: [{
|
|
id: anyUuid
|
|
}]
|
|
})
|
|
.matchHeaderSnapshot({
|
|
etag: anyEtag
|
|
});
|
|
});
|
|
|
|
it('Can delete a member without cancelling Stripe Subscription', async function () {
|
|
let subscriptionCanceled = false;
|
|
nock('https://api.stripe.com')
|
|
.persist()
|
|
.delete(/v1\/.*/)
|
|
.reply((uri) => {
|
|
const [match, resource, id] = uri.match(/\/?v1\/(\w+)(?:\/(\w+))/) || [null];
|
|
|
|
if (match && resource === 'subscriptions') {
|
|
subscriptionCanceled = true;
|
|
return [200, {
|
|
id,
|
|
status: 'canceled'
|
|
}];
|
|
}
|
|
|
|
return [500];
|
|
});
|
|
|
|
// @TODO This is wrong because it changes the state for the rest of the tests
|
|
// We need to add a member via a fixture and then remove them OR work out how
|
|
// to reapply fixtures before each test
|
|
const memberToDelete = fixtureManager.get('members', 2);
|
|
|
|
await agent
|
|
.delete(`members/${memberToDelete.id}/`)
|
|
.expectStatus(204)
|
|
.expectEmptyBody()
|
|
.matchHeaderSnapshot({
|
|
etag: anyEtag
|
|
});
|
|
|
|
assert.equal(subscriptionCanceled, false, 'expected subscription not to be canceled');
|
|
});
|
|
|
|
// Export members to CSV
|
|
|
|
it('Can export CSV', async function () {
|
|
const res = await agent
|
|
.get(`/members/upload/`)
|
|
.expectStatus(200)
|
|
.expectEmptyBody() // express-test body parsing doesn't support CSV
|
|
.matchHeaderSnapshot({
|
|
etag: anyEtag,
|
|
'content-length': anyString, //For some reason the content-length changes between 1220 and 1317
|
|
'content-disposition': anyString
|
|
});
|
|
|
|
res.text.should.match(/id,email,name,note,subscribed_to_emails,complimentary_plan,stripe_customer_id,created_at,deleted_at/);
|
|
|
|
const csv = Papa.parse(res.text, {header: true});
|
|
should.exist(csv.data.find(row => row.name === 'Mr Egg'));
|
|
should.exist(csv.data.find(row => row.name === 'Winston Zeddemore'));
|
|
should.exist(csv.data.find(row => row.name === 'Ray Stantz'));
|
|
should.exist(csv.data.find(row => row.email === 'member2@test.com'));
|
|
});
|
|
|
|
it('Can export a filtered CSV', async function () {
|
|
const res = await agent
|
|
.get(`/members/upload/?search=Egg`)
|
|
.expectStatus(200)
|
|
.expectEmptyBody() // express-test body parsing doesn't support CSV
|
|
.matchHeaderSnapshot({
|
|
etag: anyEtag,
|
|
'content-disposition': anyString
|
|
});
|
|
|
|
res.text.should.match(/id,email,name,note,subscribed_to_emails,complimentary_plan,stripe_customer_id,created_at,deleted_at/);
|
|
|
|
const csv = Papa.parse(res.text, {header: true});
|
|
should.exist(csv.data.find(row => row.name === 'Mr Egg'));
|
|
should.not.exist(csv.data.find(row => row.name === 'Egon Spengler'));
|
|
should.not.exist(csv.data.find(row => row.name === 'Ray Stantz'));
|
|
should.not.exist(csv.data.find(row => row.email === 'member2@test.com'));
|
|
});
|
|
|
|
// Get stats
|
|
|
|
it('Can fetch member counts stats', async function () {
|
|
await agent
|
|
.get(`/members/stats/count/`)
|
|
.expectStatus(200)
|
|
.matchBodySnapshot({
|
|
data: [{
|
|
date: anyISODate
|
|
}]
|
|
})
|
|
.matchHeaderSnapshot({
|
|
etag: anyEtag
|
|
});
|
|
});
|
|
|
|
it('Errors when fetching stats with unknown days param value', async function () {
|
|
await agent
|
|
.get('members/stats/?days=nope')
|
|
.expectStatus(422)
|
|
.matchHeaderSnapshot({
|
|
etag: anyEtag
|
|
})
|
|
.matchBodySnapshot({
|
|
errors: [{
|
|
id: anyErrorId
|
|
}]
|
|
});
|
|
});
|
|
|
|
it('Can filter on newsletter slug', async function () {
|
|
await agent
|
|
.get('/members/?filter=newsletters:weekly-newsletter')
|
|
.expectStatus(200)
|
|
.matchBodySnapshot({
|
|
members: new Array(4).fill(memberMatcherShallowIncludes)
|
|
})
|
|
.matchHeaderSnapshot({
|
|
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({
|
|
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({
|
|
etag: anyEtag
|
|
});
|
|
|
|
await agent
|
|
.post(`/members/`)
|
|
.body({members: [member]})
|
|
.expectStatus(422);
|
|
});
|
|
});
|