Ghost/ghost/members-importer/test/MembersCSVImporter.test.js
Chris Raible 90ebdacabb
🐛 Fixed members importer overwriting name and note if left blank (#19663)
fixes ENG-610

- Previously, when importing an existing member, if the name or note
field is left blank in the CSV file, this would overwrite (re: delete)
the existing name or note in the database.
- This change ensures that the name and note fields are only updated if
they are not blank in the CSV file.
2024-02-06 13:31:34 -08:00

825 lines
38 KiB
JavaScript

// Switch these lines once there are useful utils
// const testUtils = require('./utils');
require('./utils');
const Tier = require('@tryghost/tiers/lib/Tier');
const ObjectID = require('bson-objectid').default;
const assert = require('assert/strict');
const fs = require('fs-extra');
const path = require('path');
const sinon = require('sinon');
const MembersCSVImporter = require('../lib/MembersCSVImporter');
const csvPath = path.join(__dirname, '/fixtures/');
describe('MembersCSVImporter', function () {
let fsWriteSpy;
let memberCreateStub;
let knexStub;
let sendEmailStub;
let membersRepositoryStub;
let stripeUtilsStub;
let defaultTierId;
const defaultAllowedFields = {
email: 'email',
name: 'name',
note: 'note',
subscribed_to_emails: 'subscribed_to_emails',
created_at: 'created_at',
complimentary_plan: 'complimentary_plan',
stripe_customer_id: 'stripe_customer_id',
labels: 'labels',
import_tier: 'import_tier'
};
beforeEach(function () {
fsWriteSpy = sinon.spy(fs, 'writeFile');
});
afterEach(function () {
const writtenFile = fsWriteSpy.args?.[0]?.[0];
if (writtenFile) {
fs.removeSync(writtenFile);
}
sinon.restore();
});
const buildMockImporterInstance = (deps = {}) => {
defaultTierId = new ObjectID();
const defaultTierDummy = new Tier({
id: defaultTierId
});
memberCreateStub = sinon.stub().resolves({
id: `test_member_id`
});
membersRepositoryStub = {
get: async () => {
return null;
},
create: memberCreateStub,
update: sinon.stub().resolves(null),
linkStripeCustomer: sinon.stub().resolves(null),
getCustomerIdByEmail: sinon.stub().resolves('cus_mock_123456')
};
knexStub = {
transaction: sinon.stub().resolves({
rollback: () => {},
commit: () => {}
})
};
sendEmailStub = sinon.stub();
stripeUtilsStub = {
forceStripeSubscriptionToProduct: sinon.stub().resolves({}),
archivePrice: sinon.stub().resolves()
};
return new MembersCSVImporter({
storagePath: csvPath,
getTimezone: sinon.stub().returns('UTC'),
getMembersRepository: () => {
return membersRepositoryStub;
},
getDefaultTier: () => {
return defaultTierDummy;
},
sendEmail: sendEmailStub,
isSet: sinon.stub(),
addJob: sinon.stub(),
knex: knexStub,
urlFor: sinon.stub(),
context: {importer: true},
stripeUtils: stripeUtilsStub,
...deps
});
};
describe('process', function () {
it('should import a CSV file', async function () {
const LabelModelStub = {
findOne: sinon.stub().resolves(null)
};
const importer = buildMockImporterInstance();
const result = await importer.process({
pathToCSV: `${csvPath}/single-column-with-header.csv`,
headerMapping: defaultAllowedFields,
importLabel: {
name: 'test import'
},
user: {
email: 'test@example.com'
},
LabelModel: LabelModelStub,
verificationTrigger: {
testImportThreshold: () => {}
}
});
should.exist(result.meta);
should.exist(result.meta.stats);
should.exist(result.meta.stats.imported);
result.meta.stats.imported.should.equal(2);
should.exist(result.meta.stats.invalid);
should.equal(result.meta.import_label, null);
should.exist(result.meta.originalImportSize);
result.meta.originalImportSize.should.equal(2);
fsWriteSpy.calledOnce.should.be.true();
// Called at least once
memberCreateStub.notCalled.should.be.false();
memberCreateStub.firstCall.lastArg.context.import.should.be.true();
});
it('should import a CSV in the default Members export format', async function () {
const internalLabel = {
name: 'Test Import'
};
const LabelModelStub = {
findOne: sinon.stub()
.withArgs({
name: 'Test Import'
})
.resolves({
name: 'Test Import'
})
};
const importer = buildMockImporterInstance();
const result = await importer.process({
pathToCSV: `${csvPath}/member-csv-export.csv`,
headerMapping: defaultAllowedFields,
importLabel: {
name: 'Test Import'
},
user: {
email: 'test@example.com'
},
LabelModel: LabelModelStub,
forceInline: true,
verificationTrigger: {
testImportThreshold: () => {}
}
});
should.exist(result.meta);
should.exist(result.meta.stats);
should.exist(result.meta.stats.imported);
result.meta.stats.imported.should.equal(2);
should.exist(result.meta.stats.invalid);
should.deepEqual(result.meta.import_label, internalLabel);
should.exist(result.meta.originalImportSize);
result.meta.originalImportSize.should.equal(2);
fsWriteSpy.calledOnce.should.be.true();
// member records get inserted
membersRepositoryStub.create.calledTwice.should.be.true();
should.equal(membersRepositoryStub.create.args[0][1].context.import, true, 'inserts are done in the "import" context');
should.deepEqual(Object.keys(membersRepositoryStub.create.args[0][0]), ['email', 'name', 'note', 'subscribed', 'created_at', 'labels']);
should.equal(membersRepositoryStub.create.args[0][0].id, undefined, 'id field should not be taken from the user input');
should.equal(membersRepositoryStub.create.args[0][0].email, 'member_complimentary_test@example.com');
should.equal(membersRepositoryStub.create.args[0][0].name, 'bobby tables');
should.equal(membersRepositoryStub.create.args[0][0].note, 'a note');
should.equal(membersRepositoryStub.create.args[0][0].subscribed, true);
should.equal(membersRepositoryStub.create.args[0][0].created_at, '2022-10-18T06:34:08.000Z');
should.equal(membersRepositoryStub.create.args[0][0].deleted_at, undefined, 'deleted_at field should not be taken from the user input');
should.deepEqual(membersRepositoryStub.create.args[0][0].labels, [{
name: 'user import label'
}]);
should.deepEqual(Object.keys(membersRepositoryStub.create.args[1][0]), ['email', 'name', 'note', 'subscribed', 'created_at', 'labels']);
should.equal(membersRepositoryStub.create.args[1][0].id, undefined, 'id field should not be taken from the user input');
should.equal(membersRepositoryStub.create.args[1][0].email, 'member_stripe_test@example.com');
should.equal(membersRepositoryStub.create.args[1][0].name, 'stirpey beaver');
should.equal(membersRepositoryStub.create.args[1][0].note, 'testing notes');
should.equal(membersRepositoryStub.create.args[1][0].subscribed, false);
should.equal(membersRepositoryStub.create.args[1][0].created_at, '2022-10-18T07:31:57.000Z');
should.equal(membersRepositoryStub.create.args[1][0].deleted_at, undefined, 'deleted_at field should not be taken from the user input');
should.deepEqual(membersRepositoryStub.create.args[1][0].labels, [], 'no labels should be assigned');
// stripe customer import
membersRepositoryStub.linkStripeCustomer.calledOnce.should.be.true();
should.equal(membersRepositoryStub.linkStripeCustomer.args[0][0].customer_id, 'cus_MdR9tqW6bAreiq');
should.equal(membersRepositoryStub.linkStripeCustomer.args[0][0].member_id, 'test_member_id');
// complimentary_plan import
membersRepositoryStub.update.calledOnce.should.be.true();
should.deepEqual(membersRepositoryStub.update.args[0][0].products, [{
id: defaultTierId.toString()
}]);
should.deepEqual(membersRepositoryStub.update.args[0][1].id, 'test_member_id');
});
it('should subscribe or unsubscribe members as per the `subscribe_to_emails` column', async function () {
/**
* @NOTE This tests all the different scenarios for the `subscribed_to_emails` column for existing and new members
* For existing members with at least one newsletter subscription:
* CASE 1: If `subscribe_to_emails` is `true`, the member should remain subscribed (but not added to any additional newsletters)
* CASE 2: If `subscribe_to_emails` is `false`, the member should be unsubscribed from all newsletters
* CASE 3: If `subscribe_to_emails` is NULL, the member should remain subscribed (but not added to any additional newsletters)
* CASE 4: If `subscribe_to_emails` is empty, the member should remain subscribed (but not added to any additional newsletters)
* CASE 5: If `subscribe_to_emails` is invalid, the member should remain subscribed (but not added to any additional newsletters)
*
*
* For existing members with no newsletter subscriptions:
* CASE 6: If `subscribe_to_emails` is `true`, the member should remain unsubscribed (as they likely have previously unsubscribed)
* CASE 7: If `subscribe_to_emails` is `false`, the member should remain unsubscribed
* CASE 8: If `subscribe_to_emails` is NULL, the member should remain unsubscribed
* CASE 9: If `subscribe_to_emails` is empty, the member should remain unsubscribed
* CASE 10: If `subscribe_to_emails` is invalid, the member should remain unsubscribed
*
* - In summary, an existing member with no pre-existing newsletter subscriptions should _never_ be subscribed to newsletters upon import
*
* For new members:
* CASE 11: If `subscribe_to_emails` is `true`, the member should be created and subscribed
* CASE 12: If `subscribe_to_emails` is `false`, the member should be created but not subscribed
* CASE 13: If `subscribe_to_emails` is NULL, the member should be created and subscribed
* CASE 14: If `subscribe_to_emails` is empty, the member should be created and subscribed
* CASE 15: If `subscribe_to_emails` is invalid, the member should be created and subscribed
*/
const internalLabel = {
name: 'Test Subscription Import'
};
const LabelModelStub = {
findOne: sinon.stub()
.withArgs({
name: 'Test Subscription Import'
})
.resolves({
name: 'Test Subscription Import'
})
};
const importer = buildMockImporterInstance();
const defaultNewsletters = [
{id: 'newsletter_1'},
{id: 'newsletter_2'}
];
const existingMembers = [
{email: 'member_subscribed_true@example.com', newsletters: defaultNewsletters},
{email: 'member_subscribed_null@example.com', newsletters: defaultNewsletters},
{email: 'member_subscribed_false@example.com', newsletters: defaultNewsletters},
{email: 'member_subscribed_empty@example.com', newsletters: defaultNewsletters},
{email: 'member_subscribed_invalid@example.com', newsletters: defaultNewsletters},
{email: 'member_not_subscribed_true@example.com', newsletters: []},
{email: 'member_not_subscribed_null@example.com', newsletters: []},
{email: 'member_not_subscribed_false@example.com', newsletters: []},
{email: 'member_not_subscribed_empty@example.com', newsletters: []},
{email: 'member_not_subscribed_invalid@example.com', newsletters: []}
];
membersRepositoryStub.get = sinon.stub();
for (const existingMember of existingMembers) {
const newslettersCollection = {
length: existingMember.newsletters.length,
toJSON: sinon.stub().returns(existingMember.newsletters)
};
const memberRecord = {
related: sinon.stub()
};
memberRecord.related.withArgs('labels').returns(null);
memberRecord.related.withArgs('newsletters').returns(newslettersCollection);
membersRepositoryStub.get.withArgs({email: existingMember.email}).resolves(memberRecord);
}
const result = await importer.process({
pathToCSV: `${csvPath}/subscribed-to-emails-cases.csv`,
headerMapping: defaultAllowedFields,
importLabel: {
name: 'Test Subscription Import'
},
user: {
email: 'test@example.com'
},
LabelModel: LabelModelStub,
forceInline: true,
verificationTrigger: {
testImportThreshold: () => {}
}
});
should.exist(result.meta);
should.exist(result.meta.stats);
should.exist(result.meta.stats.imported);
result.meta.stats.imported.should.equal(5);
should.exist(result.meta.stats.invalid);
should.deepEqual(result.meta.import_label, internalLabel);
should.exist(result.meta.originalImportSize);
result.meta.originalImportSize.should.equal(15);
fsWriteSpy.calledOnce.should.be.true();
// member records get inserted
should.equal(membersRepositoryStub.create.callCount, 5);
should.equal(membersRepositoryStub.create.args[0][1].context.import, true, 'inserts are done in the "import" context');
// CASE 1: Existing member with at least one newsletter subscription, `subscribed_to_emails` column is true
// Member's newsletter subscriptions should not change
should.deepEqual(Object.keys(membersRepositoryStub.update.args[0][0]), ['email', 'name', 'note', 'subscribed', 'created_at', 'labels', 'newsletters']);
should.equal(membersRepositoryStub.update.args[0][0].email, 'member_subscribed_true@example.com');
should.equal(membersRepositoryStub.update.args[0][0].subscribed, true);
should.deepEqual(membersRepositoryStub.update.args[0][0].newsletters, defaultNewsletters);
// CASE 2: Existing member with at least one newsletter subscription, `subscribed_to_emails` column is false
// Member's newsletter subscriptions should be removed
should.deepEqual(Object.keys(membersRepositoryStub.update.args[1][0]), ['email', 'name', 'note', 'subscribed', 'created_at', 'labels']);
should.equal(membersRepositoryStub.update.args[1][0].email, 'member_subscribed_false@example.com');
should.equal(membersRepositoryStub.update.args[1][0].subscribed, false);
should.equal(membersRepositoryStub.update.args[1][0].newsletters, undefined);
// CASE 3: Existing member with at least one newsletter subscription, `subscribed_to_emails` column is NULL
// Member's newsletter subscriptions should not change
should.deepEqual(Object.keys(membersRepositoryStub.update.args[2][0]), ['email', 'name', 'note', 'subscribed', 'created_at', 'labels', 'newsletters']);
should.equal(membersRepositoryStub.update.args[2][0].email, 'member_subscribed_null@example.com');
should.equal(membersRepositoryStub.update.args[2][0].subscribed, true);
should.deepEqual(membersRepositoryStub.update.args[2][0].newsletters, defaultNewsletters);
// CASE 4: Existing member with at least one newsletter subscription, `subscribed_to_emails` column is empty
// Member's newsletter subscriptions should not change
should.deepEqual(Object.keys(membersRepositoryStub.update.args[3][0]), ['email', 'name', 'note', 'subscribed', 'created_at', 'labels', 'newsletters']);
should.equal(membersRepositoryStub.update.args[3][0].email, 'member_subscribed_empty@example.com');
should.equal(membersRepositoryStub.update.args[3][0].subscribed, true);
should.deepEqual(membersRepositoryStub.update.args[3][0].newsletters, defaultNewsletters);
// CASE 5: Existing member with at least one newsletter subscription, `subscribed_to_emails` column is invalid
// Member's newsletter subscriptions should not change
should.deepEqual(Object.keys(membersRepositoryStub.update.args[4][0]), ['email', 'name', 'note', 'subscribed', 'created_at', 'labels', 'newsletters']);
should.equal(membersRepositoryStub.update.args[4][0].email, 'member_subscribed_invalid@example.com');
should.equal(membersRepositoryStub.update.args[4][0].subscribed, true);
should.deepEqual(membersRepositoryStub.update.args[4][0].newsletters, defaultNewsletters);
// CASE 6: Existing member with no newsletter subscriptions, `subscribed_to_emails` column is true
// Member should remain unsubscribed and not added to any newsletters
should.deepEqual(Object.keys(membersRepositoryStub.update.args[5][0]), ['email', 'name', 'note', 'subscribed', 'created_at', 'labels']);
should.equal(membersRepositoryStub.update.args[5][0].email, 'member_not_subscribed_true@example.com');
should.equal(membersRepositoryStub.update.args[5][0].subscribed, false);
should.equal(membersRepositoryStub.update.args[5][0].newsletters, undefined);
// CASE 7: Existing member with no newsletter subscriptions, `subscribed_to_emails` column is false
// Member should remain unsubscribed and not added to any newsletters
should.deepEqual(Object.keys(membersRepositoryStub.update.args[6][0]), ['email', 'name', 'note', 'subscribed', 'created_at', 'labels']);
should.equal(membersRepositoryStub.update.args[6][0].email, 'member_not_subscribed_false@example.com');
should.equal(membersRepositoryStub.update.args[6][0].subscribed, false);
should.equal(membersRepositoryStub.update.args[6][0].newsletters, undefined);
// CASE 8: Existing member with no newsletter subscriptions, `subscribed_to_emails` column is NULL
// Member should remain unsubscribed and not added to any newsletters
should.deepEqual(Object.keys(membersRepositoryStub.update.args[7][0]), ['email', 'name', 'note', 'subscribed', 'created_at', 'labels']);
should.equal(membersRepositoryStub.update.args[7][0].email, 'member_not_subscribed_null@example.com');
should.equal(membersRepositoryStub.update.args[7][0].subscribed, false);
should.equal(membersRepositoryStub.update.args[7][0].newsletters, undefined);
// CASE 9: Existing member with no newsletter subscriptions, `subscribed_to_emails` column is empty
// Member should remain unsubscribed and not added to any newsletters
should.deepEqual(Object.keys(membersRepositoryStub.update.args[8][0]), ['email', 'name', 'note', 'subscribed', 'created_at', 'labels']);
should.equal(membersRepositoryStub.update.args[8][0].email, 'member_not_subscribed_empty@example.com');
should.equal(membersRepositoryStub.update.args[8][0].subscribed, false);
should.equal(membersRepositoryStub.update.args[8][0].newsletters, undefined);
// CASE 10: Existing member with no newsletter subscriptions, `subscribed_to_emails` column is invalid
// Member should remain unsubscribed and not added to any newsletters
should.deepEqual(Object.keys(membersRepositoryStub.update.args[9][0]), ['email', 'name', 'note', 'subscribed', 'created_at', 'labels']);
should.equal(membersRepositoryStub.update.args[9][0].email, 'member_not_subscribed_invalid@example.com');
should.equal(membersRepositoryStub.update.args[9][0].subscribed, false);
should.equal(membersRepositoryStub.update.args[9][0].newsletters, undefined);
// CASE 11: New member, `subscribed_to_emails` column is true
// Member should be created and subscribed
should.deepEqual(Object.keys(membersRepositoryStub.create.args[0][0]), ['email', 'name', 'note', 'subscribed', 'created_at', 'labels']);
should.equal(membersRepositoryStub.create.args[0][0].email, 'new_member_true@example.com');
should.equal(membersRepositoryStub.create.args[0][0].subscribed, true);
should.equal(membersRepositoryStub.create.args[0][0].newsletters, undefined);
// CASE 12: New member, `subscribed_to_emails` column is false
// Member should be created but not subscribed
should.deepEqual(Object.keys(membersRepositoryStub.create.args[1][0]), ['email', 'name', 'note', 'subscribed', 'created_at', 'labels']);
should.equal(membersRepositoryStub.create.args[1][0].email, 'new_member_false@example.com');
should.equal(membersRepositoryStub.create.args[1][0].subscribed, false);
should.equal(membersRepositoryStub.create.args[1][0].newsletters, undefined);
// CASE 13: New member, `subscribed_to_emails` column is NULL
// Member should be created but not subscribed
should.deepEqual(Object.keys(membersRepositoryStub.create.args[2][0]), ['email', 'name', 'note', 'subscribed', 'created_at', 'labels']);
should.equal(membersRepositoryStub.create.args[2][0].email, 'new_member_null@example.com');
should.equal(membersRepositoryStub.create.args[2][0].subscribed, true);
should.equal(membersRepositoryStub.create.args[2][0].newsletters, undefined);
// CASE 14: New member, `subscribed_to_emails` column is empty
// Member should be created but not subscribed
should.deepEqual(Object.keys(membersRepositoryStub.create.args[3][0]), ['email', 'name', 'note', 'subscribed', 'created_at', 'labels']);
should.equal(membersRepositoryStub.create.args[3][0].email, 'new_member_empty@example.com');
should.equal(membersRepositoryStub.create.args[3][0].subscribed, true);
should.equal(membersRepositoryStub.create.args[3][0].newsletters, undefined);
// CASE 15: New member, `subscribed_to_emails` column is invalid
// Member should be created but not subscribed
should.deepEqual(Object.keys(membersRepositoryStub.create.args[4][0]), ['email', 'name', 'note', 'subscribed', 'created_at', 'labels']);
should.equal(membersRepositoryStub.create.args[4][0].email, 'new_member_invalid@example.com');
should.equal(membersRepositoryStub.create.args[4][0].subscribed, true);
should.equal(membersRepositoryStub.create.args[4][0].newsletters, undefined);
});
});
describe('sendErrorEmail', function () {
it('should send email with errors for invalid CSV file', async function () {
const importer = buildMockImporterInstance();
await importer.sendErrorEmail({
emailRecipient: 'test@example.com',
emailSubject: 'Your member import was unsuccessful',
emailContent: 'Import was unsuccessful',
errorCSV: 'id,email,invalid email',
importLabel: {name: 'Test import'}
});
sendEmailStub.calledWith({
to: 'test@example.com',
subject: 'Your member import was unsuccessful',
html: 'Import was unsuccessful',
forceTextContent: true,
attachments: [
{
filename: 'Test import - Errors.csv',
content: 'id,email,invalid email',
contentType: 'text/csv',
contentDisposition: 'attachment'
}
]
}).should.be.true();
});
});
describe('prepare', function () {
it('processes a basic valid import file for members', async function () {
const membersImporter = buildMockImporterInstance();
const result = await membersImporter.prepare(`${csvPath}/single-column-with-header.csv`, defaultAllowedFields);
should.exist(result.filePath);
result.filePath.should.match(/\/members-importer\/test\/fixtures\/Members Import/);
result.batches.should.equal(2);
should.exist(result.metadata);
should.equal(result.metadata.hasStripeData, false);
fsWriteSpy.calledOnce.should.be.true();
});
it('Does not include columns not in the original CSV or mapped', async function () {
const membersImporter = buildMockImporterInstance();
await membersImporter.prepare(`${csvPath}/single-column-with-header.csv`, defaultAllowedFields);
const fileContents = fsWriteSpy.firstCall.args[1];
fileContents.should.match(/^email,labels\r\n/);
});
it('It supports "subscribed_to_emails" column header ouf of the box', async function (){
const membersImporter = buildMockImporterInstance();
await membersImporter.prepare(`${csvPath}/subscribed-to-emails-header.csv`, defaultAllowedFields);
const fileContents = fsWriteSpy.firstCall.args[1];
fileContents.should.match(/^email,subscribed_to_emails,labels\r\n/);
});
it('checks for stripe data in the imported file', async function () {
const membersImporter = buildMockImporterInstance();
const result = await membersImporter.prepare(`${csvPath}/member-csv-export.csv`);
should.exist(result.metadata);
should.equal(result.metadata.hasStripeData, true);
});
});
describe('perform', function () {
it('performs import on a single csv file', async function () {
const importer = buildMockImporterInstance();
const result = await importer.perform(`${csvPath}/single-column-with-header.csv`);
assert.equal(membersRepositoryStub.create.args[0][0].email, 'jbloggs@example.com');
assert.deepEqual(membersRepositoryStub.create.args[0][0].labels, []);
assert.equal(membersRepositoryStub.create.args[1][0].email, 'test@example.com');
assert.deepEqual(membersRepositoryStub.create.args[1][0].labels, []);
assert.equal(result.total, 2);
assert.equal(result.imported, 2);
assert.equal(result.errors.length, 0);
});
it('performs import on a csv file "subscribed_to_emails" column header', async function () {
const importer = buildMockImporterInstance();
const result = await importer.perform(`${csvPath}/subscribed-to-emails-header.csv`);
assert.equal(membersRepositoryStub.create.args[0][0].email, 'jbloggs@example.com');
assert.equal(membersRepositoryStub.create.args[0][0].subscribed, true);
assert.deepEqual(membersRepositoryStub.create.args[0][0].labels, []);
assert.equal(membersRepositoryStub.create.args[1][0].email, 'test@example.com');
assert.equal(membersRepositoryStub.create.args[1][0].subscribed, false);
assert.deepEqual(membersRepositoryStub.create.args[1][0].labels, []);
assert.equal(result.total, 2);
assert.equal(result.imported, 2);
assert.equal(result.errors.length, 0);
});
it('handles various special cases', async function () {
const importer = buildMockImporterInstance();
const result = await importer.perform(`${csvPath}/special-cases.csv`);
// CASE: Member has created_at in the future
// The member will not appear in the members list in admin
// In this case, we should overwrite create_at to current timestamp
// Refs: https://github.com/TryGhost/Team/issues/2793
assert.equal(membersRepositoryStub.create.args[0][0].email, 'timetraveler@example.com');
assert.equal(membersRepositoryStub.create.args[0][0].subscribed, true);
assert.notDeepEqual(membersRepositoryStub.create.args[0][0].created_at, '9999-10-18T06:34:08.000Z');
assert.equal(membersRepositoryStub.create.args[0][0].created_at <= new Date(), true);
assert.equal(result.total, 1);
assert.equal(result.imported, 1);
assert.equal(result.errors.length, 0);
});
it('searches for stripe customer ID by email when "auto" is passed', async function () {
const importer = buildMockImporterInstance();
const result = await importer.perform(`${csvPath}/auto-stripe-customer-id.csv`);
should.equal(membersRepositoryStub.linkStripeCustomer.args[0][0].customer_id, 'cus_mock_123456');
assert.equal(result.total, 1);
assert.equal(result.imported, 1);
assert.equal(result.errors.length, 0);
});
it('respects existing member newsletter subscription preferences', async function () {
const importer = buildMockImporterInstance();
const newsletters = [
{id: 'newsletter_1'},
{id: 'newsletter_2'}
];
const newslettersCollection = {
length: newsletters.length,
toJSON: sinon.stub().returns(newsletters)
};
const member = {
related: sinon.stub()
};
member.related.withArgs('labels').returns(null);
member.related.withArgs('newsletters').returns(newslettersCollection);
membersRepositoryStub.get = sinon.stub();
membersRepositoryStub.get
.withArgs({email: 'jbloggs@example.com'})
.resolves(member);
await importer.perform(`${csvPath}/subscribed-to-emails-header.csv`);
assert.deepEqual(membersRepositoryStub.update.args[0][0].newsletters, newsletters);
});
it('does not overwrite name or note fields for existing members when left blank in the import file', async function () {
const importer = buildMockImporterInstance();
const member = {
name: 'John Bloggs',
note: 'A note',
related: sinon.stub()
};
member.related.withArgs('labels').returns(null);
member.related.withArgs('newsletters').returns({length: 0});
membersRepositoryStub.get = sinon.stub();
membersRepositoryStub.get
.withArgs({email: 'test@example.com'})
.resolves(member);
await importer.perform(`${csvPath}/single-column-with-header.csv`);
assert.equal(membersRepositoryStub.update.args[0][0].name, 'John Bloggs');
assert.equal(membersRepositoryStub.update.args[0][0].note, 'A note');
});
it('does not add subscriptions for existing member when they do not have any subscriptions', async function () {
const importer = buildMockImporterInstance();
const member = {
related: sinon.stub()
};
member.related.withArgs('labels').returns(null);
member.related.withArgs('newsletters').returns({length: 0});
membersRepositoryStub.get = sinon.stub();
membersRepositoryStub.get
.withArgs({email: 'jbloggs@example.com'})
.resolves(member);
await importer.perform(`${csvPath}/subscribed-to-emails-header.csv`);
assert.deepEqual(membersRepositoryStub.update.args[0][0].subscribed, false);
});
it('removes existing member newsletter subscriptions when set to not be subscribed', async function () {
const importer = buildMockImporterInstance();
const newsletters = [
{id: 'newsletter_1'},
{id: 'newsletter_2'}
];
const newslettersCollection = {
length: newsletters.length,
toJSON: sinon.stub().returns(newsletters)
};
const member = {
related: sinon.stub()
};
member.related.withArgs('labels').returns(null);
member.related.withArgs('newsletters').returns(newslettersCollection);
membersRepositoryStub.get = sinon.stub();
membersRepositoryStub.get
.withArgs({email: 'test@example.com'})
.resolves(member);
await importer.perform(`${csvPath}/subscribed-to-emails-header.csv`);
assert.equal(membersRepositoryStub.update.args[0][0].subscribed, false);
assert.equal(membersRepositoryStub.update.args[0][0].newsletters, undefined);
});
it('does not import a free member with an import tier', async function () {
const tier = {
id: {
toString: () => 'abc123'
},
name: 'Premium Tier'
};
const getTierByNameStub = sinon.stub();
getTierByNameStub.withArgs(tier.name).resolves(tier);
const importer = buildMockImporterInstance({
getTierByName: getTierByNameStub
});
const result = await importer.perform(`${csvPath}/free-member-import-tier.csv`);
assert.equal(result.total, 1);
assert.equal(result.imported, 0);
assert.equal(result.errors.length, 1);
assert.equal(result.errors[0].error, 'You cannot import a free member with a specified tier.');
});
it('imports a comped member with an import tier', async function () {
const tier = {
id: {
toString: () => 'abc123'
},
name: 'Premium Tier'
};
const getTierByNameStub = sinon.stub();
getTierByNameStub.withArgs(tier.name).resolves(tier);
const importer = buildMockImporterInstance({
getTierByName: getTierByNameStub
});
const result = await importer.perform(`${csvPath}/comped-member-import-tier.csv`);
assert.equal(result.total, 1);
assert.equal(result.imported, 1);
assert.equal(result.errors.length, 0);
assert.ok(membersRepositoryStub.update.calledOnce);
assert.deepEqual(
membersRepositoryStub.update.getCall(0).args[0],
{products: [{id: tier.id.toString()}]}
);
});
it('does not import a comped member with an invalid import tier', async function () {
const tier = {
id: {
toString: () => 'abc123'
},
name: 'Premium Tier'
};
const getTierByNameStub = sinon.stub();
getTierByNameStub.withArgs(tier.name).resolves(tier);
const importer = buildMockImporterInstance({
getTierByName: getTierByNameStub
});
const result = await importer.perform(`${csvPath}/comped-member-invalid-import-tier.csv`);
assert.equal(result.total, 1);
assert.equal(result.imported, 0);
assert.equal(result.errors.length, 1);
assert.equal(result.errors[0].error, '"Invalid Tier" is not a valid tier.');
});
it('imports a paid member with an import tier', async function () {
const tier = {
id: {
toString: () => 'abc123'
},
name: 'Premium Tier'
};
const getTierByNameStub = sinon.stub();
getTierByNameStub.withArgs(tier.name).resolves(tier);
const importer = buildMockImporterInstance({
getTierByName: getTierByNameStub
});
const result = await importer.perform(`${csvPath}/paid-member-import-tier.csv`);
assert.equal(result.total, 1);
assert.equal(result.imported, 1);
assert.equal(result.errors.length, 0);
assert.ok(stripeUtilsStub.forceStripeSubscriptionToProduct.calledOnce);
assert.deepEqual(
stripeUtilsStub.forceStripeSubscriptionToProduct.getCall(0).args[0],
{
customer_id: 'cus_MdR9tqW6bAreiq',
product_id: tier.id.toString()
}
);
});
it('archives any Stripe prices created due to an import tier being specified', async function () {
const tier = {
id: {
toString: () => 'abc123'
},
name: 'Premium Tier'
};
const getTierByNameStub = sinon.stub();
getTierByNameStub.withArgs(tier.name).resolves(tier);
const newStripePriceId = 'price_123';
const importer = buildMockImporterInstance({
getTierByName: getTierByNameStub
});
stripeUtilsStub.forceStripeSubscriptionToProduct.resolves({
isNewStripePrice: true,
stripePriceId: newStripePriceId
});
const result = await importer.perform(`${csvPath}/paid-member-import-tier.csv`);
assert.equal(result.total, 1);
assert.equal(result.imported, 1);
assert.equal(result.errors.length, 0);
assert.ok(stripeUtilsStub.archivePrice.calledOnce);
assert.ok(stripeUtilsStub.archivePrice.calledWith(newStripePriceId));
});
});
});