diff --git a/config/default.json b/config/default.json index 95f5666d..b8b4a2a4 100644 --- a/config/default.json +++ b/config/default.json @@ -59,5 +59,6 @@ "inviteEmailSectionTitle": "Project Invitation", "connectUrl":"https://connect.topcoder-dev.com", "accountsAppUrl": "https://accounts.topcoder-dev.com", - "MAX_REVISION_NUMBER": 100 + "MAX_REVISION_NUMBER": 100, + "UNIQUE_GMAIL_VALIDATION": true } diff --git a/src/routes/projectMemberInvites/create.js b/src/routes/projectMemberInvites/create.js index 4839ba45..46b70399 100644 --- a/src/routes/projectMemberInvites/create.js +++ b/src/routes/projectMemberInvites/create.js @@ -28,6 +28,30 @@ const addMemberValidations = { }, }; +/** + * Helper method to check the uniqueness of two emails + * + * @param {String} email1 first email to compare + * @param {String} email2 second email to compare + * @param {Object} options the options + * + * @returns {Boolean} true if two emails are same + */ +const compareEmail = (email1, email2, options = { UNIQUE_GMAIL_VALIDATION: false }) => { + if (options.UNIQUE_GMAIL_VALIDATION) { + // email is gmail + const emailSplit = /(^[\w.+-]+)(@gmail\.com|@googlemail\.com)$/g.exec(_.toLower(email1)); + if (emailSplit) { + const address = emailSplit[1].replace('.', ''); + const emailDomain = emailSplit[2].replace('.', '\\.'); + const regexAddress = address.split('').join('\\.?'); + const regex = new RegExp(`${regexAddress}${emailDomain}`); + return regex.test(_.toLower(email2)); + } + } + return _.toLower(email1) === _.toLower(email2); +}; + /** * Helper method to build promises for creating new invites in DB * @@ -68,7 +92,8 @@ const buildCreateInvitePromises = (req, invite, invites, data, failed) => { }); // non-existent users we will invite them by email only const nonExistentUserEmails = invite.emails.filter(inviteEmail => - !_.find(existentUsers, { email: inviteEmail }), + !_.find(existentUsers, existentUser => + compareEmail(existentUser.email, inviteEmail, { UNIQUE_GMAIL_VALIDATION: false })), ); // remove invites for users that are invited already @@ -83,7 +108,9 @@ const buildCreateInvitePromises = (req, invite, invites, data, failed) => { }); // remove invites for users that are invited already - _.remove(nonExistentUserEmails, email => _.some(invites, i => i.email === email)); + _.remove(nonExistentUserEmails, email => + _.some(invites, i => + compareEmail(i.email, email, { UNIQUE_GMAIL_VALIDATION: config.get('UNIQUE_GMAIL_VALIDATION') }))); nonExistentUserEmails.forEach((email) => { const dataNew = _.clone(data); diff --git a/src/routes/projectMemberInvites/create.spec.js b/src/routes/projectMemberInvites/create.spec.js index 6965d2c4..6730fb53 100644 --- a/src/routes/projectMemberInvites/create.spec.js +++ b/src/routes/projectMemberInvites/create.spec.js @@ -73,17 +73,60 @@ describe('Project Member Invite create', () => { createdBy: 1, updatedBy: 1, }).then(() => { - models.ProjectMemberInvite.create({ - projectId: project1.id, - userId: 40051335, - email: null, - role: PROJECT_MEMBER_ROLE.MANAGER, - status: INVITE_STATUS.PENDING, - createdBy: 1, - updatedBy: 1, - createdAt: '2016-06-30 00:33:07+00', - updatedAt: '2016-06-30 00:33:07+00', - }).then(() => { + const promises = [ + models.ProjectMemberInvite.create({ + projectId: project1.id, + userId: 40051335, + email: null, + role: PROJECT_MEMBER_ROLE.MANAGER, + status: INVITE_STATUS.PENDING, + createdBy: 1, + updatedBy: 1, + createdAt: '2016-06-30 00:33:07+00', + updatedAt: '2016-06-30 00:33:07+00', + }), + models.ProjectMemberInvite.create({ + projectId: project1.id, + email: 'duplicate_lowercase@test.com', + role: PROJECT_MEMBER_ROLE.MANAGER, + status: INVITE_STATUS.PENDING, + createdBy: 1, + updatedBy: 1, + createdAt: '2016-06-30 00:33:07+00', + updatedAt: '2016-06-30 00:33:07+00', + }), + models.ProjectMemberInvite.create({ + projectId: project1.id, + email: 'DUPLICATE_UPPERCASE@test.com', + role: PROJECT_MEMBER_ROLE.MANAGER, + status: INVITE_STATUS.PENDING, + createdBy: 1, + updatedBy: 1, + createdAt: '2016-06-30 00:33:07+00', + updatedAt: '2016-06-30 00:33:07+00', + }), + models.ProjectMemberInvite.create({ + projectId: project1.id, + email: 'with.dot@gmail.com', + role: PROJECT_MEMBER_ROLE.MANAGER, + status: INVITE_STATUS.PENDING, + createdBy: 1, + updatedBy: 1, + createdAt: '2016-06-30 00:33:07+00', + updatedAt: '2016-06-30 00:33:07+00', + }), + models.ProjectMemberInvite.create({ + projectId: project1.id, + email: 'withoutdot@gmail.com', + role: PROJECT_MEMBER_ROLE.MANAGER, + status: INVITE_STATUS.PENDING, + createdBy: 1, + updatedBy: 1, + createdAt: '2016-06-30 00:33:07+00', + updatedAt: '2016-06-30 00:33:07+00', + }), + ]; + Promise.all(promises).then(() => { done(); }); }); @@ -640,6 +683,112 @@ describe('Project Member Invite create', () => { }); }); + it('should return 201 and empty response when trying add already invited member by lowercase email', (done) => { + request(server) + .post(`/v4/projects/${project1.id}/members/invite`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .send({ + param: { + emails: ['DUPLICATE_LOWERCASE@test.com'], + role: 'customer', + }, + }) + .expect('Content-Type', /json/) + .expect(201) + .end((err, res) => { + if (err) { + done(err); + } else { + const resJson = res.body.result.content.success; + should.exist(resJson); + resJson.length.should.equal(0); + done(); + } + }); + }); + + it('should return 201 and empty response when trying add already invited member by uppercase email', (done) => { + request(server) + .post(`/v4/projects/${project1.id}/members/invite`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .send({ + param: { + emails: ['duplicate_uppercase@test.com'], + role: 'customer', + }, + }) + .expect('Content-Type', /json/) + .expect(201) + .end((err, res) => { + if (err) { + done(err); + } else { + const resJson = res.body.result.content.success; + should.exist(resJson); + resJson.length.should.equal(0); + done(); + } + }); + }); + + it('should return 201 and empty response when trying add already invited member by gmail email with dot', + (done) => { + request(server) + .post(`/v4/projects/${project1.id}/members/invite`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .send({ + param: { + emails: ['WITHdot@gmail.com'], + role: 'customer', + }, + }) + .expect('Content-Type', /json/) + .expect(201) + .end((err, res) => { + if (err) { + done(err); + } else { + const resJson = res.body.result.content.success; + should.exist(resJson); + resJson.length.should.equal(0); + done(); + } + }); + }); + + it('should return 201 and empty response when trying add already invited member by gmail email without dot', + (done) => { + request(server) + .post(`/v4/projects/${project1.id}/members/invite`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .send({ + param: { + emails: ['WITHOUT.dot@gmail.com'], + role: 'customer', + }, + }) + .expect('Content-Type', /json/) + .expect(201) + .end((err, res) => { + if (err) { + done(err); + } else { + const resJson = res.body.result.content.success; + should.exist(resJson); + resJson.length.should.equal(0); + done(); + } + }); + }); + describe('Bus api', () => { let createEventSpy;