Skip to content

Commit 5a9cd64

Browse files
author
vikasrohit
authored
Merge pull request #303 from topcoder-platform/feature/fix-email-duplicated-invites
Feature/fix email duplicated invites
2 parents 0b66bce + 5d29438 commit 5a9cd64

File tree

3 files changed

+191
-14
lines changed

3 files changed

+191
-14
lines changed

config/default.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,5 +59,6 @@
5959
"inviteEmailSectionTitle": "Project Invitation",
6060
"connectUrl":"https://connect.topcoder-dev.com",
6161
"accountsAppUrl": "https://accounts.topcoder-dev.com",
62-
"MAX_REVISION_NUMBER": 100
62+
"MAX_REVISION_NUMBER": 100,
63+
"UNIQUE_GMAIL_VALIDATION": true
6364
}

src/routes/projectMemberInvites/create.js

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,30 @@ const addMemberValidations = {
2828
},
2929
};
3030

31+
/**
32+
* Helper method to check the uniqueness of two emails
33+
*
34+
* @param {String} email1 first email to compare
35+
* @param {String} email2 second email to compare
36+
* @param {Object} options the options
37+
*
38+
* @returns {Boolean} true if two emails are same
39+
*/
40+
const compareEmail = (email1, email2, options = { UNIQUE_GMAIL_VALIDATION: false }) => {
41+
if (options.UNIQUE_GMAIL_VALIDATION) {
42+
// email is gmail
43+
const emailSplit = /(^[\w.+-]+)(@gmail\.com|@googlemail\.com)$/g.exec(_.toLower(email1));
44+
if (emailSplit) {
45+
const address = emailSplit[1].replace('.', '');
46+
const emailDomain = emailSplit[2].replace('.', '\\.');
47+
const regexAddress = address.split('').join('\\.?');
48+
const regex = new RegExp(`${regexAddress}${emailDomain}`);
49+
return regex.test(_.toLower(email2));
50+
}
51+
}
52+
return _.toLower(email1) === _.toLower(email2);
53+
};
54+
3155
/**
3256
* Helper method to build promises for creating new invites in DB
3357
*
@@ -68,7 +92,8 @@ const buildCreateInvitePromises = (req, invite, invites, data, failed) => {
6892
});
6993
// non-existent users we will invite them by email only
7094
const nonExistentUserEmails = invite.emails.filter(inviteEmail =>
71-
!_.find(existentUsers, { email: inviteEmail }),
95+
!_.find(existentUsers, existentUser =>
96+
compareEmail(existentUser.email, inviteEmail, { UNIQUE_GMAIL_VALIDATION: false })),
7297
);
7398

7499
// remove invites for users that are invited already
@@ -83,7 +108,9 @@ const buildCreateInvitePromises = (req, invite, invites, data, failed) => {
83108
});
84109

85110
// remove invites for users that are invited already
86-
_.remove(nonExistentUserEmails, email => _.some(invites, i => i.email === email));
111+
_.remove(nonExistentUserEmails, email =>
112+
_.some(invites, i =>
113+
compareEmail(i.email, email, { UNIQUE_GMAIL_VALIDATION: config.get('UNIQUE_GMAIL_VALIDATION') })));
87114
nonExistentUserEmails.forEach((email) => {
88115
const dataNew = _.clone(data);
89116

src/routes/projectMemberInvites/create.spec.js

Lines changed: 160 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -73,17 +73,60 @@ describe('Project Member Invite create', () => {
7373
createdBy: 1,
7474
updatedBy: 1,
7575
}).then(() => {
76-
models.ProjectMemberInvite.create({
77-
projectId: project1.id,
78-
userId: 40051335,
79-
email: null,
80-
role: PROJECT_MEMBER_ROLE.MANAGER,
81-
status: INVITE_STATUS.PENDING,
82-
createdBy: 1,
83-
updatedBy: 1,
84-
createdAt: '2016-06-30 00:33:07+00',
85-
updatedAt: '2016-06-30 00:33:07+00',
86-
}).then(() => {
76+
const promises = [
77+
models.ProjectMemberInvite.create({
78+
projectId: project1.id,
79+
userId: 40051335,
80+
email: null,
81+
role: PROJECT_MEMBER_ROLE.MANAGER,
82+
status: INVITE_STATUS.PENDING,
83+
createdBy: 1,
84+
updatedBy: 1,
85+
createdAt: '2016-06-30 00:33:07+00',
86+
updatedAt: '2016-06-30 00:33:07+00',
87+
}),
88+
models.ProjectMemberInvite.create({
89+
projectId: project1.id,
90+
email: 'duplicate_lowercase@test.com',
91+
role: PROJECT_MEMBER_ROLE.MANAGER,
92+
status: INVITE_STATUS.PENDING,
93+
createdBy: 1,
94+
updatedBy: 1,
95+
createdAt: '2016-06-30 00:33:07+00',
96+
updatedAt: '2016-06-30 00:33:07+00',
97+
}),
98+
models.ProjectMemberInvite.create({
99+
projectId: project1.id,
100+
email: 'DUPLICATE_UPPERCASE@test.com',
101+
role: PROJECT_MEMBER_ROLE.MANAGER,
102+
status: INVITE_STATUS.PENDING,
103+
createdBy: 1,
104+
updatedBy: 1,
105+
createdAt: '2016-06-30 00:33:07+00',
106+
updatedAt: '2016-06-30 00:33:07+00',
107+
}),
108+
models.ProjectMemberInvite.create({
109+
projectId: project1.id,
110+
email: 'with.dot@gmail.com',
111+
role: PROJECT_MEMBER_ROLE.MANAGER,
112+
status: INVITE_STATUS.PENDING,
113+
createdBy: 1,
114+
updatedBy: 1,
115+
createdAt: '2016-06-30 00:33:07+00',
116+
updatedAt: '2016-06-30 00:33:07+00',
117+
}),
118+
models.ProjectMemberInvite.create({
119+
projectId: project1.id,
120+
email: 'withoutdot@gmail.com',
121+
role: PROJECT_MEMBER_ROLE.MANAGER,
122+
status: INVITE_STATUS.PENDING,
123+
createdBy: 1,
124+
updatedBy: 1,
125+
createdAt: '2016-06-30 00:33:07+00',
126+
updatedAt: '2016-06-30 00:33:07+00',
127+
}),
128+
];
129+
Promise.all(promises).then(() => {
87130
done();
88131
});
89132
});
@@ -640,6 +683,112 @@ describe('Project Member Invite create', () => {
640683
});
641684
});
642685

686+
it('should return 201 and empty response when trying add already invited member by lowercase email', (done) => {
687+
request(server)
688+
.post(`/v4/projects/${project1.id}/members/invite`)
689+
.set({
690+
Authorization: `Bearer ${testUtil.jwts.copilot}`,
691+
})
692+
.send({
693+
param: {
694+
emails: ['DUPLICATE_LOWERCASE@test.com'],
695+
role: 'customer',
696+
},
697+
})
698+
.expect('Content-Type', /json/)
699+
.expect(201)
700+
.end((err, res) => {
701+
if (err) {
702+
done(err);
703+
} else {
704+
const resJson = res.body.result.content.success;
705+
should.exist(resJson);
706+
resJson.length.should.equal(0);
707+
done();
708+
}
709+
});
710+
});
711+
712+
it('should return 201 and empty response when trying add already invited member by uppercase email', (done) => {
713+
request(server)
714+
.post(`/v4/projects/${project1.id}/members/invite`)
715+
.set({
716+
Authorization: `Bearer ${testUtil.jwts.copilot}`,
717+
})
718+
.send({
719+
param: {
720+
emails: ['duplicate_uppercase@test.com'],
721+
role: 'customer',
722+
},
723+
})
724+
.expect('Content-Type', /json/)
725+
.expect(201)
726+
.end((err, res) => {
727+
if (err) {
728+
done(err);
729+
} else {
730+
const resJson = res.body.result.content.success;
731+
should.exist(resJson);
732+
resJson.length.should.equal(0);
733+
done();
734+
}
735+
});
736+
});
737+
738+
it('should return 201 and empty response when trying add already invited member by gmail email with dot',
739+
(done) => {
740+
request(server)
741+
.post(`/v4/projects/${project1.id}/members/invite`)
742+
.set({
743+
Authorization: `Bearer ${testUtil.jwts.copilot}`,
744+
})
745+
.send({
746+
param: {
747+
emails: ['WITHdot@gmail.com'],
748+
role: 'customer',
749+
},
750+
})
751+
.expect('Content-Type', /json/)
752+
.expect(201)
753+
.end((err, res) => {
754+
if (err) {
755+
done(err);
756+
} else {
757+
const resJson = res.body.result.content.success;
758+
should.exist(resJson);
759+
resJson.length.should.equal(0);
760+
done();
761+
}
762+
});
763+
});
764+
765+
it('should return 201 and empty response when trying add already invited member by gmail email without dot',
766+
(done) => {
767+
request(server)
768+
.post(`/v4/projects/${project1.id}/members/invite`)
769+
.set({
770+
Authorization: `Bearer ${testUtil.jwts.copilot}`,
771+
})
772+
.send({
773+
param: {
774+
emails: ['WITHOUT.dot@gmail.com'],
775+
role: 'customer',
776+
},
777+
})
778+
.expect('Content-Type', /json/)
779+
.expect(201)
780+
.end((err, res) => {
781+
if (err) {
782+
done(err);
783+
} else {
784+
const resJson = res.body.result.content.success;
785+
should.exist(resJson);
786+
resJson.length.should.equal(0);
787+
done();
788+
}
789+
});
790+
});
791+
643792
describe('Bus api', () => {
644793
let createEventSpy;
645794

0 commit comments

Comments
 (0)