mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-23 19:02:29 +03:00
Added default newsletter subscription for new members (#14431)
refs https://github.com/TryGhost/Team/issues/1469 Currently, all new members get auto subscribed to the default newsletter. This change adds same behavior with multiple newsletters by auto subscribing all available newsletters on site for new members(If flag is enabled). Note: In future, this will also take into consideration the `subscribe_on_signup` flag for a newsletter to filter which newsletters should a member be auto-subscribed. - adds newsletters service for working with newsletter data - bumps `@tryghost/members-api` package which handles default subscription - adds new test fixture/data for newsletters
This commit is contained in:
parent
7b01bd0209
commit
90e7887007
@ -13,6 +13,7 @@ const SingleUseTokenProvider = require('./SingleUseTokenProvider');
|
||||
const urlUtils = require('../../../shared/url-utils');
|
||||
const labsService = require('../../../shared/labs');
|
||||
const offersService = require('../offers');
|
||||
const getNewslettersServiceInstance = require('../newsletters');
|
||||
|
||||
const MAGIC_LINK_TOKEN_VALIDITY = 24 * 60 * 60 * 1000;
|
||||
|
||||
@ -195,7 +196,8 @@ function createApiInstance(config) {
|
||||
},
|
||||
stripeAPIService: stripeService.api,
|
||||
offersAPI: offersService.api,
|
||||
labsService: labsService
|
||||
labsService: labsService,
|
||||
newslettersService: getNewslettersServiceInstance({NewsletterModel: models.Newsletter})
|
||||
});
|
||||
|
||||
return membersApiInstance;
|
||||
|
10
core/server/services/newsletters/index.js
Normal file
10
core/server/services/newsletters/index.js
Normal file
@ -0,0 +1,10 @@
|
||||
const NewslettersService = require('./service.js');
|
||||
|
||||
/**
|
||||
* @returns {NewslettersService} instance of the NewslettersService
|
||||
*/
|
||||
const getNewslettersServiceInstance = ({NewsletterModel}) => {
|
||||
return new NewslettersService({NewsletterModel});
|
||||
};
|
||||
|
||||
module.exports = getNewslettersServiceInstance;
|
24
core/server/services/newsletters/service.js
Normal file
24
core/server/services/newsletters/service.js
Normal file
@ -0,0 +1,24 @@
|
||||
class NewslettersService {
|
||||
/**
|
||||
*
|
||||
* @param {Object} options
|
||||
* @param {Object} options.NewsletterModel
|
||||
*/
|
||||
constructor({NewsletterModel}) {
|
||||
this.NewsletterModel = NewsletterModel;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Object} options browse options
|
||||
* @returns
|
||||
*/
|
||||
async browse(options) {
|
||||
let newsletters = await this.NewsletterModel.findAll(options);
|
||||
|
||||
return newsletters.toJSON();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = NewslettersService;
|
||||
|
@ -83,7 +83,7 @@
|
||||
"@tryghost/logging": "2.1.2",
|
||||
"@tryghost/magic-link": "1.0.21",
|
||||
"@tryghost/member-events": "0.4.1",
|
||||
"@tryghost/members-api": "5.5.0",
|
||||
"@tryghost/members-api": "5.6.0",
|
||||
"@tryghost/members-events-service": "0.3.2",
|
||||
"@tryghost/members-importer": "0.5.6",
|
||||
"@tryghost/members-offers": "0.10.9",
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -322,7 +322,7 @@ describe('Members API', function () {
|
||||
memberId: newMember.id,
|
||||
asserts: [
|
||||
{
|
||||
from_status: null,
|
||||
from_status: null,
|
||||
to_status: 'free'
|
||||
}
|
||||
]
|
||||
@ -590,7 +590,7 @@ describe('Members API', function () {
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
const {body} = await agent
|
||||
.post(`/members/`)
|
||||
.body({members: [initialMember]})
|
||||
@ -773,7 +773,7 @@ describe('Members API', function () {
|
||||
return [200, fakeSubscription];
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
const initialMember = {
|
||||
name: fakeCustomer.name,
|
||||
email: fakeCustomer.email,
|
||||
@ -896,7 +896,7 @@ describe('Members API', function () {
|
||||
return [200, fakeSubscription];
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
const initialMember = {
|
||||
name: fakeCustomer.name,
|
||||
email: fakeCustomer.email,
|
||||
@ -1290,12 +1290,11 @@ describe('Members API', function () {
|
||||
describe('Members API: with multiple newsletters', function () {
|
||||
before(async function () {
|
||||
agent = await agentProvider.getAdminAPIAgent();
|
||||
await fixtureManager.init('members');
|
||||
await fixtureManager.init('members', 'newsletters');
|
||||
await agent.loginAsOwner();
|
||||
});
|
||||
|
||||
beforeEach(function () {
|
||||
mockManager.mockLabsEnabled('multipleProducts');
|
||||
mockManager.mockLabsEnabled('multipleNewsletters');
|
||||
mockManager.mockStripe();
|
||||
mockManager.mockMail();
|
||||
@ -1318,147 +1317,6 @@ describe('Members API: with multiple newsletters', function () {
|
||||
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(memberMatcherShallowIncludesForNewsletters)
|
||||
})
|
||||
.matchHeaderSnapshot({
|
||||
etag: anyEtag
|
||||
});
|
||||
});
|
||||
|
||||
it('Can browse with search', async function () {
|
||||
await agent
|
||||
.get('/members/?search=member1')
|
||||
.expectStatus(200)
|
||||
.matchBodySnapshot({
|
||||
members: new Array(1).fill(memberMatcherShallowIncludesForNewsletters)
|
||||
})
|
||||
.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(memberMatcherShallowIncludesForNewsletters)
|
||||
})
|
||||
.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(memberMatcherShallowIncludesForNewsletters)
|
||||
})
|
||||
.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(memberMatcherShallowIncludesForNewsletters)
|
||||
})
|
||||
.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(memberMatcherShallowIncludesForNewsletters)
|
||||
})
|
||||
.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(memberMatcherShallowIncludesForNewsletters)
|
||||
})
|
||||
.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: [memberMatcherShallowIncludesForNewsletters]
|
||||
})
|
||||
.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: [memberMatcherShallowIncludesForNewsletters]
|
||||
})
|
||||
.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: [memberMatcherShallowIncludesForNewsletters]
|
||||
})
|
||||
.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 () {
|
||||
@ -1473,575 +1331,103 @@ describe('Members API: with multiple newsletters', function () {
|
||||
});
|
||||
});
|
||||
|
||||
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(memberMatcherShallowIncludesForNewsletters)
|
||||
})
|
||||
.matchHeaderSnapshot({
|
||||
etag: anyEtag
|
||||
});
|
||||
});
|
||||
|
||||
// Create a member
|
||||
|
||||
it('Can add', async function () {
|
||||
it('Can add with default newsletters', async function () {
|
||||
const member = {
|
||||
name: 'test',
|
||||
email: 'memberTestAdd@test.com',
|
||||
email: 'memberTestNewsletterAdd@test.com',
|
||||
note: 'test note',
|
||||
subscribed: false,
|
||||
labels: ['test-label']
|
||||
};
|
||||
|
||||
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({
|
||||
id: matchers.anyObjectId
|
||||
})
|
||||
}]
|
||||
})
|
||||
.matchHeaderSnapshot({
|
||||
etag: anyEtag,
|
||||
location: anyLocationFor('members')
|
||||
});
|
||||
});
|
||||
|
||||
// Edit a member
|
||||
it('Can and 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: new Array(1).fill(memberMatcherShallowIncludesForNewsletters)
|
||||
members: [{
|
||||
id: anyObjectId,
|
||||
uuid: anyUuid,
|
||||
created_at: anyISODateTime,
|
||||
updated_at: anyISODateTime,
|
||||
subscriptions: anyArray,
|
||||
labels: anyArray,
|
||||
newsletters: Array(1).fill({
|
||||
id: matchers.anyObjectId
|
||||
})
|
||||
}]
|
||||
})
|
||||
.matchHeaderSnapshot({
|
||||
etag: anyEtag,
|
||||
location: anyLocationFor('members')
|
||||
});
|
||||
|
||||
const newMember = body.members[0];
|
||||
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({
|
||||
id: matchers.anyObjectId
|
||||
})
|
||||
}]
|
||||
})
|
||||
.matchHeaderSnapshot({
|
||||
etag: anyEtag
|
||||
});
|
||||
|
||||
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', 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: [memberMatcherNoIncludes]
|
||||
})
|
||||
.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('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', 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',
|
||||
subscribed: true
|
||||
};
|
||||
|
||||
const compedPayload = {
|
||||
comped: true
|
||||
};
|
||||
|
||||
const {body} = await agent
|
||||
.post(`/members/`)
|
||||
.body({members: [initialMember]})
|
||||
.expectStatus(201)
|
||||
.matchBodySnapshot({
|
||||
members: new Array(1).fill(memberMatcherShallowIncludesForNewsletters)
|
||||
})
|
||||
.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(memberMatcherShallowIncludesForNewsletters)
|
||||
})
|
||||
.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'
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
it('Can edit by id', async function () {
|
||||
const memberToChange = {
|
||||
name: 'change me',
|
||||
email: 'member2Change@test.com',
|
||||
note: 'initial note',
|
||||
subscribed: true
|
||||
};
|
||||
|
||||
const memberChanged = {
|
||||
name: 'changed',
|
||||
email: 'cantChangeMe@test.com',
|
||||
note: 'edited note',
|
||||
subscribed: false
|
||||
};
|
||||
|
||||
const {body} = await agent
|
||||
.post(`/members/`)
|
||||
.body({members: [memberToChange]})
|
||||
.expectStatus(201)
|
||||
.matchBodySnapshot({
|
||||
members: new Array(1).fill(memberMatcherShallowIncludesForNewsletters)
|
||||
})
|
||||
.matchHeaderSnapshot({
|
||||
etag: anyEtag,
|
||||
location: anyLocationFor('members')
|
||||
});
|
||||
const newMember = body.members[0];
|
||||
|
||||
await assertMemberEvents({
|
||||
eventType: 'MemberSubscribeEvent',
|
||||
memberId: newMember.id,
|
||||
asserts: [
|
||||
{
|
||||
subscribed: true,
|
||||
source: 'admin'
|
||||
}
|
||||
]
|
||||
});
|
||||
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(memberMatcherShallowIncludesForNewsletters)
|
||||
})
|
||||
.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'
|
||||
},
|
||||
{
|
||||
subscribed: false,
|
||||
source: 'admin'
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}]
|
||||
})
|
||||
})
|
||||
.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
|
||||
}
|
||||
}
|
||||
}]
|
||||
})
|
||||
})
|
||||
.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(memberMatcherShallowIncludesForNewsletters)
|
||||
})
|
||||
.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
|
||||
}]
|
||||
});
|
||||
});
|
||||
});
|
||||
|
106
test/unit/server/services/members/utils.test.js
Normal file
106
test/unit/server/services/members/utils.test.js
Normal file
@ -0,0 +1,106 @@
|
||||
const sinon = require('sinon');
|
||||
const should = require('should');
|
||||
const {formattedMemberResponse} = require('../../../../../core/server/services/members/utils');
|
||||
const labs = require('../../../../../core/shared/labs');
|
||||
|
||||
describe('Members Service - utils', function () {
|
||||
describe('formattedMemberResponse', function () {
|
||||
let labsStub;
|
||||
beforeEach(function () {
|
||||
labsStub = sinon.stub(labs, 'isSet').returns(true);
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
it('returns correct data', async function () {
|
||||
const member1 = formattedMemberResponse({
|
||||
uuid: 'uuid-1',
|
||||
email: 'jamie+1@example.com',
|
||||
name: 'Jamie Larson',
|
||||
avatar_image: 'https://gravatar.com/avatar/7d8efd2c2a781111599a8cae293cf704?s=250&d=blank',
|
||||
subscribed: true,
|
||||
status: 'free',
|
||||
extra: 'property'
|
||||
});
|
||||
should(member1).deepEqual({
|
||||
uuid: 'uuid-1',
|
||||
email: 'jamie+1@example.com',
|
||||
name: 'Jamie Larson',
|
||||
firstname: 'Jamie',
|
||||
avatar_image: 'https://gravatar.com/avatar/7d8efd2c2a781111599a8cae293cf704?s=250&d=blank',
|
||||
subscribed: true,
|
||||
subscriptions: [],
|
||||
paid: false
|
||||
});
|
||||
});
|
||||
|
||||
it('formats newsletter data', async function () {
|
||||
const member1 = formattedMemberResponse({
|
||||
uuid: 'uuid-1',
|
||||
email: 'jamie+1@example.com',
|
||||
name: 'Jamie Larson',
|
||||
avatar_image: 'https://gravatar.com/avatar/7d8efd2c2a781111599a8cae293cf704?s=250&d=blank',
|
||||
subscribed: true,
|
||||
status: 'comped',
|
||||
extra: 'property',
|
||||
newsletters: [{
|
||||
id: 'newsletter-1',
|
||||
name: 'Daily brief',
|
||||
description: 'One email daily',
|
||||
sender_name: 'Jamie',
|
||||
sender_email: 'jamie@example.com',
|
||||
sort_order: 0
|
||||
}]
|
||||
});
|
||||
should(member1).deepEqual({
|
||||
uuid: 'uuid-1',
|
||||
email: 'jamie+1@example.com',
|
||||
name: 'Jamie Larson',
|
||||
firstname: 'Jamie',
|
||||
avatar_image: 'https://gravatar.com/avatar/7d8efd2c2a781111599a8cae293cf704?s=250&d=blank',
|
||||
subscribed: true,
|
||||
subscriptions: [],
|
||||
paid: true,
|
||||
newsletters: [{
|
||||
id: 'newsletter-1',
|
||||
name: 'Daily brief',
|
||||
description: 'One email daily',
|
||||
sort_order: 0
|
||||
}]
|
||||
});
|
||||
});
|
||||
|
||||
it('removes newsletter data if flag is disabled', async function () {
|
||||
labsStub.returns(false);
|
||||
const member1 = formattedMemberResponse({
|
||||
uuid: 'uuid-1',
|
||||
email: 'jamie+1@example.com',
|
||||
name: 'Jamie Larson',
|
||||
avatar_image: 'https://gravatar.com/avatar/7d8efd2c2a781111599a8cae293cf704?s=250&d=blank',
|
||||
subscribed: true,
|
||||
status: 'paid',
|
||||
extra: 'property',
|
||||
newsletters: [{
|
||||
id: 'newsletter-1',
|
||||
name: 'Daily brief',
|
||||
description: 'One email daily',
|
||||
sender_name: 'Jamie',
|
||||
sender_email: 'jamie@example.com',
|
||||
sort_order: 0
|
||||
}]
|
||||
});
|
||||
should(member1).deepEqual({
|
||||
uuid: 'uuid-1',
|
||||
email: 'jamie+1@example.com',
|
||||
name: 'Jamie Larson',
|
||||
firstname: 'Jamie',
|
||||
avatar_image: 'https://gravatar.com/avatar/7d8efd2c2a781111599a8cae293cf704?s=250&d=blank',
|
||||
subscribed: true,
|
||||
subscriptions: [],
|
||||
paid: true
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
43
test/unit/server/services/newsletters/index.test.js
Normal file
43
test/unit/server/services/newsletters/index.test.js
Normal file
@ -0,0 +1,43 @@
|
||||
const should = require('should');
|
||||
const sinon = require('sinon');
|
||||
const getNewslettersServiceInstance = require('../../../../../core/server/services/newsletters');
|
||||
const models = require('../../../../../core/server/models');
|
||||
|
||||
describe('Newsletters Service', function () {
|
||||
before(function () {
|
||||
models.init();
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
describe('browse', function () {
|
||||
it('lists all newsletters', async function () {
|
||||
const findAllStub = {
|
||||
toJSON: function () {
|
||||
return [
|
||||
{
|
||||
id: 'newsletter-1'
|
||||
},
|
||||
{
|
||||
id: 'newsletter-2'
|
||||
}
|
||||
];
|
||||
}
|
||||
};
|
||||
sinon.stub(models.Newsletter, 'findAll').returns(Promise.resolve(findAllStub));
|
||||
|
||||
const NewslettersService = getNewslettersServiceInstance({NewsletterModel: models.Newsletter});
|
||||
const newsletters = await NewslettersService.browse({});
|
||||
should(newsletters).deepEqual([
|
||||
{
|
||||
id: 'newsletter-1'
|
||||
},
|
||||
{
|
||||
id: 'newsletter-2'
|
||||
}
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
@ -577,6 +577,12 @@ const fixtures = {
|
||||
});
|
||||
},
|
||||
|
||||
insertNewsletters: async function insertNewsletters() {
|
||||
return Promise.map(DataGenerator.forKnex.newsletters, function (newsletter) {
|
||||
return models.Newsletter.add(newsletter, context.internal);
|
||||
});
|
||||
},
|
||||
|
||||
insertSnippets: function insertSnippets() {
|
||||
return Promise.map(DataGenerator.forKnex.snippets, function (snippet) {
|
||||
return models.Snippet.add(snippet, context.internal);
|
||||
@ -630,6 +636,9 @@ const toDoList = {
|
||||
members: function insertMembersAndLabelsAndProducts() {
|
||||
return fixtures.insertMembersAndLabelsAndProducts();
|
||||
},
|
||||
newsletters: function insertNewsletters() {
|
||||
return fixtures.insertNewsletters();
|
||||
},
|
||||
'members:emails': function insertEmailsAndRecipients() {
|
||||
return fixtures.insertEmailsAndRecipients();
|
||||
},
|
||||
|
@ -365,6 +365,25 @@ DataGenerator.Content = {
|
||||
}
|
||||
],
|
||||
|
||||
newsletters: [
|
||||
{
|
||||
id: ObjectId().toHexString(),
|
||||
name: 'Daily newsletter',
|
||||
description: '',
|
||||
sender_name: 'Jamie',
|
||||
sender_email: 'jamie@example.com',
|
||||
sender_reply_to: 'jamie@example.com'
|
||||
},
|
||||
{
|
||||
id: ObjectId().toHexString(),
|
||||
name: 'Weekly newsletter',
|
||||
description: '',
|
||||
sender_name: 'Jamie',
|
||||
sender_email: 'jamie@example.com',
|
||||
sender_reply_to: 'jamie@example.com'
|
||||
}
|
||||
],
|
||||
|
||||
products: [
|
||||
{
|
||||
// No ID because these are in the core fixtures.json
|
||||
@ -888,6 +907,23 @@ DataGenerator.forKnex = (function () {
|
||||
};
|
||||
}
|
||||
|
||||
function createNewsletter(overrides) {
|
||||
const newObj = _.cloneDeep(overrides);
|
||||
|
||||
return _.defaults(newObj, {
|
||||
id: ObjectId().toHexString(),
|
||||
name: 'Daily Newsletter',
|
||||
sender_name: 'Jamie Larsen',
|
||||
sender_email: 'jamie@example.com',
|
||||
sender_reply_to: 'jamie@example.com',
|
||||
default: false,
|
||||
status: 'active',
|
||||
recipient_filter: '',
|
||||
subscribe_on_signup: true,
|
||||
sort_order: 0
|
||||
});
|
||||
}
|
||||
|
||||
function createMember(overrides) {
|
||||
const newObj = _.cloneDeep(overrides);
|
||||
|
||||
@ -1169,6 +1205,14 @@ DataGenerator.forKnex = (function () {
|
||||
}
|
||||
];
|
||||
|
||||
const members_newsletters = [
|
||||
{
|
||||
id: ObjectId().toHexString(),
|
||||
member_id: DataGenerator.Content.posts[0].id,
|
||||
newsletter_id: DataGenerator.Content.tags[0].id
|
||||
}
|
||||
];
|
||||
|
||||
const posts_authors = [
|
||||
{
|
||||
id: ObjectId().toHexString(),
|
||||
@ -1280,6 +1324,11 @@ DataGenerator.forKnex = (function () {
|
||||
createMember(DataGenerator.Content.members[7])
|
||||
];
|
||||
|
||||
const newsletters = [
|
||||
createNewsletter(DataGenerator.Content.newsletters[0]),
|
||||
createNewsletter(DataGenerator.Content.newsletters[1])
|
||||
];
|
||||
|
||||
const labels = [
|
||||
createLabel(DataGenerator.Content.labels[0]),
|
||||
createLabel(DataGenerator.Content.labels[2])
|
||||
@ -1369,6 +1418,7 @@ DataGenerator.forKnex = (function () {
|
||||
createEmail,
|
||||
createCustomThemeSetting: createBasic,
|
||||
createProduct,
|
||||
createNewsletter,
|
||||
|
||||
invites,
|
||||
posts,
|
||||
@ -1376,6 +1426,7 @@ DataGenerator.forKnex = (function () {
|
||||
posts_meta,
|
||||
posts_tags,
|
||||
posts_authors,
|
||||
members_newsletters,
|
||||
roles,
|
||||
users,
|
||||
roles_users,
|
||||
@ -1388,6 +1439,7 @@ DataGenerator.forKnex = (function () {
|
||||
labels,
|
||||
members,
|
||||
products,
|
||||
newsletters,
|
||||
members_labels,
|
||||
members_stripe_customers,
|
||||
stripe_customer_subscriptions,
|
||||
|
12
yarn.lock
12
yarn.lock
@ -2069,10 +2069,10 @@
|
||||
"@tryghost/domain-events" "^0.1.9"
|
||||
"@tryghost/member-events" "^0.4.1"
|
||||
|
||||
"@tryghost/members-api@5.5.0":
|
||||
version "5.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/members-api/-/members-api-5.5.0.tgz#30bf580051f8ca69aa3c31515e83a4348af97f91"
|
||||
integrity sha512-Mwlt3F/bFcGqnnJ9Y4fBkTiQS7tb1++puuO1fh35BNpOL5TuEdYF9kxjkODqnseEioaeWNxigsdx2yvvzgcwPA==
|
||||
"@tryghost/members-api@5.6.0":
|
||||
version "5.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/members-api/-/members-api-5.6.0.tgz#af84c73d273eb8321909ff23d457d1144916d838"
|
||||
integrity sha512-XOn0eZVNy9IK7F9SkTTMyw59/iqYEsmRiAvZ6DPQbGL2O+FXIUdGnuGw1lJ2q33QWxBDpZ63oyj6rByVZiHu5A==
|
||||
dependencies:
|
||||
"@nexes/nql" "^0.6.0"
|
||||
"@tryghost/debug" "^0.1.2"
|
||||
@ -2084,7 +2084,7 @@
|
||||
"@tryghost/member-events" "^0.4.1"
|
||||
"@tryghost/members-analytics-ingress" "^0.1.12"
|
||||
"@tryghost/members-payments" "^0.1.11"
|
||||
"@tryghost/members-stripe-service" "^0.9.3"
|
||||
"@tryghost/members-stripe-service" "^0.9.4"
|
||||
"@tryghost/tpl" "^0.1.2"
|
||||
"@types/jsonwebtoken" "^8.5.1"
|
||||
bluebird "^3.5.4"
|
||||
@ -2156,7 +2156,7 @@
|
||||
jsonwebtoken "^8.5.1"
|
||||
lodash "^4.17.11"
|
||||
|
||||
"@tryghost/members-stripe-service@0.9.4", "@tryghost/members-stripe-service@^0.9.3":
|
||||
"@tryghost/members-stripe-service@0.9.4", "@tryghost/members-stripe-service@^0.9.4":
|
||||
version "0.9.4"
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/members-stripe-service/-/members-stripe-service-0.9.4.tgz#2e07cfa31dddba1d2bdde2d6b7449bc2c4bc6118"
|
||||
integrity sha512-22e7IlNx3D49YK4bdi3wknQ0Tz1nW9zqpq4MIlbLYjGeC8zBR1v1LSMG7NyQR8chErgp8u51xnC6YyQgDd9iAw==
|
||||
|
Loading…
Reference in New Issue
Block a user