0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-01-20 22:42:53 -05:00

🐛 Fixed empty error csv file for member imports (#15274)

closes https://github.com/TryGhost/Team/issues/1828

- members importer was sending empty error files in case of invalid rows in CSV, hiding both error and affected rows
- fixes typo in `content` option(was `contents` before) passed to attachment (ref - https://nodemailer.com/message/attachments )
This commit is contained in:
Rishabh Garg 2022-08-24 00:49:30 +05:30 committed by GitHub
parent a3d4c4b3a9
commit c16abbf085
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 104 additions and 12 deletions

View file

@ -258,6 +258,34 @@ module.exports = class MembersCSVImporter {
return membersCSV.unparse(errorsWithFormattedMessages);
}
/**
* Send email with attached CSV containing error rows info
*
* @param {Object} config
* @param {String} config.emailRecipient - email recipient for error file
* @param {String} config.emailSubject - email subject
* @param {String} config.emailContent - html content of email
* @param {String} config.errorCSV - error CSV content
* @param {Object} config.emailSubject - email subject
* @param {Object} config.importLabel -
* @param {String} config.importLabel.name - label name
*/
async sendErrorEmail({emailRecipient, emailSubject, emailContent, errorCSV, importLabel}) {
await this._sendEmail({
to: emailRecipient,
subject: emailSubject,
html: emailContent,
forceTextContent: true,
attachments: [{
filename: `${importLabel.name} - Errors.csv`,
content: errorCSV,
contentType: 'text/csv',
contentDisposition: 'attachment'
}]
});
return;
}
/**
* Processes CSV file and imports member&label records depending on the size of the imported set
*
@ -303,17 +331,12 @@ module.exports = class MembersCSVImporter {
const errorCSV = this.generateErrorCSV(result);
const emailSubject = result.imported > 0 ? 'Your member import is complete' : 'Your member import was unsuccessful';
await this._sendEmail({
to: emailRecipient,
subject: emailSubject,
html: emailContent,
forceTextContent: true,
attachments: [{
filename: `${importLabel.name} - Errors.csv`,
contents: errorCSV,
contentType: 'text/csv',
contentDisposition: 'attachment'
}]
await this.sendErrorEmail({
emailRecipient,
emailSubject,
emailContent,
errorCSV,
importLabel
});
},
offloaded: false

View file

@ -17,7 +17,7 @@ describe('Importer', function () {
});
afterEach(function () {
const writtenFile = fsWriteSpy.args[0][0];
const writtenFile = fsWriteSpy.args?.[0]?.[0];
if (writtenFile) {
fs.removeSync(writtenFile);
@ -103,6 +103,75 @@ describe('Importer', function () {
});
});
describe('sendErrorEmail', function () {
it('should send email with errors for invalid CSV file', async function () {
const defaultProduct = {
id: 'default_product_id'
};
const memberCreateStub = sinon.stub().resolves(null);
const membersApi = {
productRepository: {
list: async () => {
return {
data: [defaultProduct]
};
}
},
members: {
get: async () => {
return null;
},
create: memberCreateStub
}
};
const knexStub = {
transaction: sinon.stub().resolves({
rollback: () => {},
commit: () => {}
})
};
const sendEmailStub = sinon.stub();
const importer = new MembersCSVImporter({
storagePath: csvPath,
getTimezone: sinon.stub().returns('UTC'),
getMembersApi: () => membersApi,
sendEmail: sendEmailStub,
isSet: sinon.stub(),
addJob: sinon.stub(),
knex: knexStub,
urlFor: sinon.stub(),
context: {importer: true}
});
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 = new MembersCSVImporter({