6161f94910
refs: https://github.com/TryGhost/Toolbox/issues/595 We're rolling out new rules around the node assert library, the first of which is enforcing the use of assert/strict. This means we don't need to use the strict version of methods, as the standard version will work that way by default. This caught some gotchas in our existing usage of assert where the lack of strict mode had unexpected results: - Url matching needs to be done on `url.href` seeaa58b354a4
- Null and undefined are not the same thing, there were a few cases of this being confused - Particularly questionable changes in [PostExporter tests](c1a468744b
) tracked [here](https://github.com/TryGhost/Team/issues/3505). - A typo seeeaac9c293a
Moving forward, using assert strict should help us to catch unexpected behaviour, particularly around nulls and undefineds during implementation.
301 lines
10 KiB
JavaScript
301 lines
10 KiB
JavaScript
const {agentProvider, fixtureManager, mockManager, configUtils} = require('../../utils/e2e-framework');
|
|
const assert = require('assert/strict');
|
|
const nock = require('nock');
|
|
const sinon = require('sinon');
|
|
const models = require('../../../core/server/models');
|
|
const moment = require('moment');
|
|
const logging = require('@tryghost/logging');
|
|
|
|
const milestonesService = require('../../../core/server/services/milestones');
|
|
|
|
let agent;
|
|
let counter = 0;
|
|
let membersCounter = 0;
|
|
const fifteenDays = 1000 * 60 * 60 * 24 * 15;
|
|
|
|
async function createMemberWithSubscription(interval, amount, currency, date) {
|
|
counter += 1;
|
|
membersCounter += 1;
|
|
|
|
const fakePrice = {
|
|
id: 'price_' + counter,
|
|
product: '',
|
|
active: true,
|
|
nickname: 'Paid',
|
|
unit_amount: amount,
|
|
currency,
|
|
type: 'recurring',
|
|
recurring: {
|
|
interval
|
|
}
|
|
};
|
|
|
|
const fakeSubscription = {
|
|
id: 'sub_' + counter,
|
|
customer: 'cus_' + counter,
|
|
status: 'active',
|
|
cancel_at_period_end: false,
|
|
metadata: {},
|
|
current_period_end: Math.floor(Date.now() / 1000 + 1000),
|
|
start_date: moment(date).unix(),
|
|
plan: fakePrice,
|
|
items: {
|
|
data: [{
|
|
price: fakePrice
|
|
}]
|
|
}
|
|
};
|
|
|
|
const fakeCustomer = {
|
|
id: 'cus_' + counter,
|
|
name: 'Test Member',
|
|
email: 'create-member-subscription-' + counter + '@email.com',
|
|
subscriptions: {
|
|
type: 'list',
|
|
data: [fakeSubscription]
|
|
}
|
|
};
|
|
nock('https://api.stripe.com')
|
|
.persist()
|
|
.get(/v1\/.*/)
|
|
.reply((uri) => {
|
|
const [match, resource] = 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,
|
|
stripe_customer_id: fakeCustomer.id
|
|
};
|
|
|
|
await agent
|
|
.post(`/members/`)
|
|
.body({members: [initialMember]})
|
|
.expectStatus(201);
|
|
|
|
nock.cleanAll();
|
|
}
|
|
|
|
async function createFreeMembers(amount, amountImported = 0) {
|
|
const members = [];
|
|
|
|
const newsletters = await agent.get(`/newsletters/`);
|
|
const newsletter = newsletters.body?.newsletters?.[0];
|
|
|
|
for (let index = 0; index < amount; index++) {
|
|
let membersAddRequest;
|
|
membersCounter += 1;
|
|
|
|
const member = {
|
|
name: 'Test Member',
|
|
email: 'free-member-' + membersCounter + '@email.com',
|
|
status: 'free',
|
|
uuid: `f6f91461-d7d8-4a3f-aa5d-8e582c40b99${membersCounter}`
|
|
};
|
|
|
|
if (amountImported > 0) {
|
|
member.subscribed = true;
|
|
member.newsletters = [newsletter];
|
|
|
|
const createMemberEvent = await agent
|
|
.post(`/members/`)
|
|
.body({members: [member]})
|
|
.expectStatus(201);
|
|
|
|
const id = createMemberEvent.body.members[0].id;
|
|
|
|
// Manually add the members_subscribe_event so we can test imported members
|
|
const editedEvent = await models.MemberSubscribeEvent.add({
|
|
newsletter_id: newsletter.id,
|
|
member_id: id,
|
|
subscribed: true,
|
|
source: index < amountImported ? 'import' : 'member'
|
|
});
|
|
|
|
membersAddRequest = Promise.all([createMemberEvent, editedEvent]);
|
|
} else {
|
|
membersAddRequest = await agent
|
|
.post(`/members/`)
|
|
.body({members: [member]})
|
|
.expectStatus(201);
|
|
}
|
|
|
|
members.push(membersAddRequest);
|
|
}
|
|
|
|
await Promise.all(members);
|
|
}
|
|
|
|
describe('Milestones Service', function () {
|
|
let clock;
|
|
let loggingStub;
|
|
|
|
const milestonesConfig = {
|
|
arr: [{currency: 'usd', values: [0, 100, 150]}],
|
|
members: [0, 10, 20, 30],
|
|
minDaysSinceImported: 7,
|
|
minDaysSinceLastEmail: 14
|
|
};
|
|
|
|
before(async function () {
|
|
agent = await agentProvider.getAdminAPIAgent();
|
|
await fixtureManager.init('newsletters');
|
|
await agent.loginAsOwner();
|
|
});
|
|
|
|
beforeEach(async function () {
|
|
loggingStub = sinon.stub(logging, 'info');
|
|
const threeMonthsAgo = new Date();
|
|
threeMonthsAgo.setMonth(threeMonthsAgo.getMonth() - 3);
|
|
clock = sinon.useFakeTimers({
|
|
now: threeMonthsAgo.getTime(),
|
|
shouldAdvanceTime: true,
|
|
toFake: ['setTimeout']
|
|
});
|
|
configUtils.set('milestones', milestonesConfig);
|
|
mockManager.mockMail();
|
|
});
|
|
|
|
afterEach(async function () {
|
|
await configUtils.restore();
|
|
mockManager.restore();
|
|
clock.restore();
|
|
clock = undefined;
|
|
sinon.restore();
|
|
});
|
|
|
|
it('Inits milestone service', async function () {
|
|
await milestonesService.init();
|
|
|
|
assert.ok(milestonesService.api);
|
|
});
|
|
|
|
it('Runs ARR and Members milestone jobs', async function () {
|
|
mockManager.mockSetting('stripe_connect_publishable_key', 'pk_live_89843uihsidfh98832uo8ri');
|
|
|
|
// No ARR and no members
|
|
const firstResultPromise = milestonesService.initAndRun(fifteenDays);
|
|
await clock.tickAsync(fifteenDays);
|
|
const firstRun = await firstResultPromise;
|
|
assert(firstRun.members.value === 0);
|
|
assert(firstRun.arr === undefined);
|
|
|
|
await createFreeMembers(7);
|
|
await createMemberWithSubscription('year', 5000, 'usd', '2000-01-10');
|
|
await createMemberWithSubscription('month', 100, 'usd', '2000-01-10');
|
|
const secondResultPromise = milestonesService.initAndRun(fifteenDays);
|
|
await clock.tickAsync(fifteenDays);
|
|
const secondRun = await secondResultPromise;
|
|
|
|
assert(secondRun.members.value === 0);
|
|
assert(secondRun.arr.value === 0);
|
|
|
|
// Reached the first milestone for members
|
|
await createFreeMembers(1);
|
|
const thirdResultPromise = milestonesService.initAndRun(fifteenDays);
|
|
await clock.tickAsync(fifteenDays);
|
|
const thirdRun = await thirdResultPromise;
|
|
|
|
assert(thirdRun.members.value === 10);
|
|
assert(thirdRun.members.emailSentAt !== undefined);
|
|
assert(thirdRun.arr.value === 0);
|
|
|
|
const memberMilestoneModel = await models.Milestone.findOne({value: 10, type: 'members'});
|
|
|
|
assert.ok(memberMilestoneModel.get('id'));
|
|
assert.equal(memberMilestoneModel.get('type'), 'members');
|
|
assert.equal(memberMilestoneModel.get('value'), 10);
|
|
assert.ok(memberMilestoneModel.get('created_at'));
|
|
assert.ok(memberMilestoneModel.get('email_sent_at'));
|
|
|
|
// Reached the first milestone for ARR
|
|
// - Members already reached, no new one will be created
|
|
// - Less than two weeks since last milestone email, no email will be sent
|
|
await createMemberWithSubscription('month', 500, 'usd', '2000-01-10');
|
|
await createMemberWithSubscription('month', 500, 'eur', '2000-01-10');
|
|
await createMemberWithSubscription('year', 1000, 'usd', '2000-01-10');
|
|
const fourthResultPromise = milestonesService.initAndRun(100);
|
|
await clock.tickAsync(100);
|
|
const fourthRun = await fourthResultPromise;
|
|
assert(fourthRun.members.value === 10);
|
|
assert(fourthRun.arr.value === 100);
|
|
assert(fourthRun.arr.emailSentAt !== undefined);
|
|
|
|
const arrMilestoneModel = await models.Milestone.findOne({value: 100, type: 'arr'});
|
|
|
|
assert.ok(arrMilestoneModel.get('id'));
|
|
assert.equal(arrMilestoneModel.get('type'), 'arr');
|
|
assert.equal(arrMilestoneModel.get('value'), 100);
|
|
assert.ok(arrMilestoneModel.get('created_at'));
|
|
assert.equal(arrMilestoneModel.get('email_sent_at'), null);
|
|
|
|
assert(loggingStub.called);
|
|
});
|
|
|
|
it('Does not run ARR milestones when Stripe is not live enabled', async function () {
|
|
mockManager.mockSetting('stripe_publishable_key', 'pk_test_89843uihsidfh98832uo8ri');
|
|
mockManager.mockSetting('stripe_connect_publishable_key', 'pk_test_89843uihsidfh98832uo8ri');
|
|
await createFreeMembers(12);
|
|
|
|
const resultPromise = milestonesService.initAndRun(fifteenDays);
|
|
await clock.tickAsync(fifteenDays);
|
|
const result = await resultPromise;
|
|
assert(result.members.value === 20);
|
|
assert(result.members.emailSentAt !== undefined);
|
|
assert(result.arr === undefined);
|
|
|
|
const memberMilestoneModel = await models.Milestone.findOne({value: 20, type: 'members'});
|
|
|
|
assert.ok(memberMilestoneModel.get('id'));
|
|
assert.equal(memberMilestoneModel.get('type'), 'members');
|
|
assert.equal(memberMilestoneModel.get('value'), 20);
|
|
assert.ok(memberMilestoneModel.get('created_at'));
|
|
assert.equal(memberMilestoneModel.get('email_sent_at'), null);
|
|
|
|
assert(loggingStub.called);
|
|
});
|
|
|
|
it('Does not send emails for milestones when imported members present', async function () {
|
|
mockManager.mockSetting('stripe_publishable_key', 'pk_live_89843uihsidfh98832uo8ri');
|
|
mockManager.mockSetting('stripe_connect_publishable_key', 'pk_test_89843uihsidfh98832uo8ri');
|
|
|
|
await createFreeMembers(10, 1);
|
|
const resultPromise = milestonesService.initAndRun(fifteenDays);
|
|
|
|
await clock.tickAsync(fifteenDays);
|
|
const result = await resultPromise;
|
|
|
|
assert(result.members.value === 30);
|
|
assert(result.members.emailSentAt === null);
|
|
|
|
assert(result.arr.value === 100);
|
|
|
|
const memberMilestoneModel = await models.Milestone.findOne({value: 30, type: 'members'});
|
|
|
|
assert.ok(memberMilestoneModel.get('id'));
|
|
assert.equal(memberMilestoneModel.get('type'), 'members');
|
|
assert.equal(memberMilestoneModel.get('value'), 30);
|
|
assert.ok(memberMilestoneModel.get('created_at'));
|
|
assert.equal(memberMilestoneModel.get('email_sent_at'), null);
|
|
|
|
const arrMilestoneModel = await models.Milestone.findOne({value: 150, type: 'arr'});
|
|
|
|
assert.equal(arrMilestoneModel, null);
|
|
|
|
assert(loggingStub.called);
|
|
});
|
|
});
|