0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-02-10 23:36:14 -05:00
ghost/test/e2e-api/admin/members-exporter.test.js
Hannah Wolfe 8dd009ffa0
🐛 Fixed member exports timing out for large sites (#14876) (#14878)
refs TryGhost/Team#1641

This commit adds a custom query for the members export, to improve the performance and to prevent any timeouts from happening when exporting large amounts of members.

Co-authored-by: Simon Backx <simon@ghost.org>
Co-authored-by: Matt Hanley <git@matthanley.co.uk>
2022-05-20 21:25:23 +01:00

239 lines
7.8 KiB
JavaScript

const {agentProvider, mockManager, fixtureManager, matchers} = require('../../utils/e2e-framework');
const {anyEtag, anyString} = matchers;
const uuid = require('uuid');
const should = require('should');
const Papa = require('papaparse');
const models = require('../../../core/server/models');
const moment = require('moment');
async function createMember(data) {
const member = await models.Member.add({
email: uuid.v4() + '@example.com',
name: '',
...data
});
return member;
}
let agent;
let tiers, labels, newsletters;
function basicAsserts(member, row) {
// Basic checks
should(row.email).eql(member.get('email'));
should(row.name).eql(member.get('name'));
should(row.note).eql(member.get('note') || '');
should(row.deleted_at).eql('');
should(row.created_at).eql(moment(member.get('created_at')).toISOString());
}
/**
*
* @param {(row: any) => void} asserts
*/
async function testOutput(member, asserts, filters = []) {
// Add default filters that always should match
filters.push('limit=all');
filters.push(`filter=id:${member.id}`);
for (const filter of filters) {
// Test all
let res = await agent
.get(`/members/upload/?${filter}`)
.expectStatus(200)
.expectEmptyBody()
.matchHeaderSnapshot({
etag: anyEtag,
'content-length': anyString,
'content-disposition': anyString
});
res.text.should.match(/id,email,name,note,subscribed_to_emails,complimentary_plan,stripe_customer_id,created_at,deleted_at,labels,products/);
let csv = Papa.parse(res.text, {header: true});
let row = csv.data.find(r => r.id === member.id);
should.exist(row);
asserts(row);
if (filter === 'filter=id:${member.id}') {
csv.data.length.should.eql(1);
}
}
}
describe('Members API — exportCSV', function () {
before(async function () {
agent = await agentProvider.getAdminAPIAgent();
await fixtureManager.init('newsletters', 'tiers:archived');
await agent.loginAsOwner();
await models.Product.add({
name: 'Extra Paid Product',
slug: 'extra-product',
type: 'paid',
active: true,
visibility: 'public'
});
tiers = (await models.Product.findAll()).models.filter(m => m.get('type') === 'paid');
tiers.length.should.be.greaterThan(1, 'These tests requires at least two paid tiers');
await models.Label.add({
name: 'Label A'
});
await models.Label.add({
name: 'Label B'
});
labels = (await models.Label.findAll()).models;
labels.length.should.be.greaterThan(1, 'These tests requires at least two labels');
newsletters = (await models.Newsletter.findAll()).models;
newsletters.length.should.be.greaterThan(1, 'These tests requires at least two newsletters');
});
beforeEach(function () {
mockManager.mockStripe();
mockManager.mockMail();
});
afterEach(function () {
mockManager.restore();
});
it('Can export products', async function () {
// Create a new member with a product
const member = await createMember({
name: 'Test member',
products: tiers
});
const tiersList = tiers.map(tier => tier.get('name')).sort().join(',');
await testOutput(member, (row) => {
basicAsserts(member, row);
should(row.subscribed_to_emails).eql('false');
should(row.complimentary_plan).eql('');
should(row.products.split(',').sort().join(',')).eql(tiersList);
}, [`filter=products:${tiers[0].get('slug')}`, 'filter=subscribed:false']);
});
it('Can export a member without products', async function () {
// Create a new member with a product
const member = await createMember({
name: 'Test member 2',
note: 'Just a note 2'
});
await testOutput(member, (row) => {
basicAsserts(member, row);
should(row.subscribed_to_emails).eql('false');
should(row.complimentary_plan).eql('');
should(row.products).eql('');
}, ['filter=subscribed:false']);
});
it('Can export labels', async function () {
// Create a new member with a product
const member = await createMember({
name: 'Test member',
note: 'Just a note',
labels: labels.map((l) => {
return {
name: l.get('name')
};
})
});
const labelsList = labels.map(label => label.get('name')).join(',');
await testOutput(member, (row) => {
basicAsserts(member, row);
should(row.subscribed_to_emails).eql('false');
should(row.complimentary_plan).eql('');
should(row.labels).eql(labelsList);
should(row.products).eql('');
}, [`filter=label:${labels[0].get('slug')}`, 'filter=subscribed:false']);
});
it('Can export comped', async function () {
// Create a new member with a product
const member = await createMember({
name: 'Test member',
note: 'Just a note',
status: 'comped'
});
await testOutput(member, (row) => {
basicAsserts(member, row);
should(row.subscribed_to_emails).eql('false');
should(row.complimentary_plan).eql('true');
should(row.labels).eql('');
should(row.products).eql('');
}, ['filter=status:comped', 'filter=subscribed:false']);
});
it('Can export newsletters', async function () {
// Create a new member with a product
const member = await createMember({
name: 'Test member',
note: 'Just a note',
newsletters: [{
id: newsletters[0].id
}]
});
await testOutput(member, (row) => {
basicAsserts(member, row);
should(row.subscribed_to_emails).eql('true');
should(row.complimentary_plan).eql('');
should(row.labels).eql('');
should(row.products).eql('');
}, ['filter=subscribed:true']);
});
it('Can export customer id', async function () {
// Create a new member with a product
const member = await createMember({
name: 'Test member',
note: 'Just a note'
});
const customer = await models.MemberStripeCustomer.add({
member_id: member.id,
customer_id: 'cus_12345',
name: 'Test member',
email: member.get('email')
});
// NOTE: we need to create a subscription here because of the way the customer id is currently fetched
const subscription = await models.StripeCustomerSubscription.add({
subscription_id: 'sub_123',
customer_id: customer.get('customer_id'),
stripe_price_id: 'price_123',
status: 'active',
cancel_at_period_end: false,
current_period_end: '2023-05-19 09:08:53',
start_date: '2020-05-19 09:08:53',
plan_id: 'price_1L15K4JQCtFaIJka01folNVK',
plan_nickname: 'Yearly',
plan_interval: 'year',
plan_amount: 5000,
plan_currency: 'USD'
});
await testOutput(member, (row) => {
basicAsserts(member, row);
should(row.subscribed_to_emails).eql('false');
should(row.complimentary_plan).eql('');
should(row.labels).eql('');
should(row.products).eql('');
should(row.stripe_customer_id).eql('cus_12345');
}, ['filter=subscribed:false', 'filter=subscriptions.subscription_id:sub_123']);
});
});