Skip to content

Commit 811a860

Browse files
authored
Merge pull request #258 from maxceem/feature/individual-invitation-errors
Feature/individual invitation errors
2 parents 1f27184 + 5106789 commit 811a860

File tree

4 files changed

+167
-71
lines changed

4 files changed

+167
-71
lines changed

postman.json

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1041,6 +1041,31 @@
10411041
},
10421042
"response": []
10431043
},
1044+
{
1045+
"name": "Invite with userIds and emails - both success and failed",
1046+
"request": {
1047+
"url": "{{api-url}}/v4/projects/1/members/invite",
1048+
"method": "POST",
1049+
"header": [
1050+
{
1051+
"key": "Authorization",
1052+
"value": "Bearer {{jwt-token-manager-40051334}}",
1053+
"description": ""
1054+
},
1055+
{
1056+
"key": "Content-Type",
1057+
"value": "application/json",
1058+
"description": ""
1059+
}
1060+
],
1061+
"body": {
1062+
"mode": "raw",
1063+
"raw": "{\n\t\"param\": {\n\t\t\"userIds\": [40051331, 40051334],\n\t\t\"emails\": [\"divyalife526@gmail.com\"],\n\t\t\"role\": \"manager\"\n\t}\n}"
1064+
},
1065+
"description": ""
1066+
},
1067+
"response": []
1068+
},
10441069
{
10451070
"name": "Update invite status with userId",
10461071
"request": {
@@ -5497,4 +5522,4 @@
54975522
]
54985523
}
54995524
]
5500-
}
5525+
}

src/routes/projectMemberInvites/create.js

Lines changed: 45 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,12 @@ const addMemberValidations = {
3535
* @param {Object} invite invite to process
3636
* @param {Array} invites existent invites from DB
3737
* @param {Object} data template for new invites to be put in DB
38+
* @param {Array} failed failed invites error message
3839
*
3940
* @returns {Promise<Promise[]>} list of promises
4041
*/
41-
const buildCreateInvitePromises = (req, invite, invites, data) => {
42+
const buildCreateInvitePromises = (req, invite, invites, data, failed) => {
4243
const invitePromises = [];
43-
4444
if (invite.userIds) {
4545
// remove invites for users that are invited already
4646
_.remove(invite.userIds, u => _.some(invites, i => i.userId === u));
@@ -91,15 +91,15 @@ const buildCreateInvitePromises = (req, invite, invites, data) => {
9191

9292
invitePromises.push(models.ProjectMemberInvite.create(dataNew));
9393
});
94-
95-
return Promise.resolve(invitePromises);
94+
return invitePromises;
9695
}).catch((error) => {
9796
req.log.error(error);
98-
return Promise.reject(invitePromises);
97+
_.forEach(invite.emails, email => failed.push(_.assign({}, { email, message: error.statusText })));
98+
return invitePromises;
9999
});
100100
}
101101

102-
return Promise.resolve(invitePromises);
102+
return invitePromises;
103103
};
104104

105105
const sendInviteEmail = (req, projectId, invite) => {
@@ -157,6 +157,7 @@ module.exports = [
157157
validate(addMemberValidations),
158158
permissions('projectMemberInvite.create'),
159159
(req, res, next) => {
160+
let failed = [];
160161
const invite = req.body.param;
161162

162163
if (!invite.userIds && !invite.emails) {
@@ -192,12 +193,11 @@ module.exports = [
192193
if (invite.emails) {
193194
// email invites can only be used for CUSTOMER role
194195
if (invite.role !== PROJECT_MEMBER_ROLE.CUSTOMER) { // eslint-disable-line no-lonely-if
195-
const err = new Error(`Emails can only be used for ${PROJECT_MEMBER_ROLE.CUSTOMER}`);
196-
err.status = 400;
197-
return next(err);
196+
const message = `Emails can only be used for ${PROJECT_MEMBER_ROLE.CUSTOMER}`;
197+
failed = _.concat(failed, _.map(invite.emails, email => _.assign({}, { email, message })));
198+
delete invite.emails;
198199
}
199200
}
200-
201201
if (promises.length === 0) {
202202
promises.push(Promise.resolve());
203203
}
@@ -209,14 +209,14 @@ module.exports = [
209209
const [userId, roles] = data;
210210
req.log.debug(roles);
211211

212-
if (!util.hasIntersection(MANAGER_ROLES, roles)) {
212+
if (roles && !util.hasIntersection(MANAGER_ROLES, roles)) {
213213
forbidUserList.push(userId);
214214
}
215215
});
216216
if (forbidUserList.length > 0) {
217-
const err = new Error(`${forbidUserList.join()} cannot be added with a Manager role to the project`);
218-
err.status = 403;
219-
return next(err);
217+
const message = 'cannot be added with a Manager role to the project';
218+
failed = _.concat(failed, _.map(forbidUserList, id => _.assign({}, { userId: id, message })));
219+
invite.userIds = _.filter(invite.userIds, userId => !_.includes(forbidUserList, userId));
220220
}
221221
}
222222
return models.ProjectMemberInvite.getPendingInvitesForProject(projectId)
@@ -233,39 +233,39 @@ module.exports = [
233233
updatedBy: req.authUser.userId,
234234
};
235235

236-
return buildCreateInvitePromises(req, invite, invites, data)
237-
.then((invitePromises) => {
238-
if (invitePromises.length === 0) {
239-
return [];
240-
}
241-
242-
req.log.debug('Creating invites');
243-
return models.sequelize.Promise.all(invitePromises)
244-
.then((values) => {
245-
values.forEach((v) => {
246-
req.app.emit(EVENT.ROUTING_KEY.PROJECT_MEMBER_INVITE_CREATED, {
247-
req,
248-
userId: v.userId,
249-
email: v.email,
250-
status: v.status,
251-
role: v.role,
252-
});
253-
req.app.services.pubsub.publish(
254-
EVENT.ROUTING_KEY.PROJECT_MEMBER_INVITE_CREATED,
255-
v,
256-
{ correlationId: req.id },
257-
);
258-
// send email invite (async)
259-
if (v.email && !v.userId && v.status === INVITE_STATUS.PENDING) {
260-
sendInviteEmail(req, projectId, v);
261-
}
262-
});
263-
return values;
264-
}); // models.sequelize.Promise.all
265-
}); // buildCreateInvitePromises
236+
req.log.debug('Creating invites');
237+
return models.sequelize.Promise.all(buildCreateInvitePromises(req, invite, invites, data, failed))
238+
.then((values) => {
239+
values.forEach((v) => {
240+
req.app.emit(EVENT.ROUTING_KEY.PROJECT_MEMBER_INVITE_CREATED, {
241+
req,
242+
userId: v.userId,
243+
email: v.email,
244+
status: v.status,
245+
role: v.role,
246+
});
247+
req.app.services.pubsub.publish(
248+
EVENT.ROUTING_KEY.PROJECT_MEMBER_INVITE_CREATED,
249+
v,
250+
{ correlationId: req.id },
251+
);
252+
// send email invite (async)
253+
if (v.email && !v.userId && v.status === INVITE_STATUS.PENDING) {
254+
sendInviteEmail(req, projectId, v);
255+
}
256+
});
257+
return values;
258+
}); // models.sequelize.Promise.all
266259
}); // models.ProjectMemberInvite.getPendingInvitesForProject
267260
})
268-
.then(values => res.status(201).json(util.wrapResponse(req.id, values, null, 201)))
261+
.then((values) => {
262+
const success = _.assign({}, { success: values });
263+
if (failed.length) {
264+
res.status(403).json(util.wrapResponse(req.id, _.assign({}, success, { failed }), null, 403));
265+
} else {
266+
res.status(201).json(util.wrapResponse(req.id, success, null, 201));
267+
}
268+
})
269269
.catch(err => next(err));
270270
},
271271
];

src/routes/projectMemberInvites/create.spec.js

Lines changed: 85 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,15 @@ describe('Project Member Invite create', () => {
4242
createdBy: 1,
4343
updatedBy: 1,
4444
});
45+
46+
models.ProjectMember.create({
47+
userId: 40051334,
48+
projectId: project1.id,
49+
role: 'manager',
50+
isPrimary: true,
51+
createdBy: 1,
52+
updatedBy: 1,
53+
});
4554
}).then(() =>
4655
models.Project.create({
4756
type: 'generic',
@@ -87,6 +96,7 @@ describe('Project Member Invite create', () => {
8796
sinon.stub(server.services.pubsub, 'init', () => {});
8897
sinon.stub(server.services.pubsub, 'publish', () => {});
8998
// by default mock lookupUserEmails return nothing so all the cases are not broken
99+
sandbox.stub(util, 'getUserRoles', () => Promise.resolve([]));
90100
sandbox.stub(util, 'lookupUserEmails', () => Promise.resolve([]));
91101
sandbox.stub(util, 'getMemberDetailsByUserIds', () => Promise.resolve([{
92102
userId: 40051333,
@@ -246,9 +256,11 @@ describe('Project Member Invite create', () => {
246256
result: {
247257
success: true,
248258
status: 200,
249-
content: [{
250-
roleName: USER_ROLE.COPILOT,
251-
}],
259+
content: {
260+
success: [{
261+
roleName: USER_ROLE.COPILOT,
262+
}],
263+
},
252264
},
253265
},
254266
}),
@@ -271,7 +283,7 @@ describe('Project Member Invite create', () => {
271283
if (err) {
272284
done(err);
273285
} else {
274-
const resJson = res.body.result.content[0];
286+
const resJson = res.body.result.content.success[0];
275287
should.exist(resJson);
276288
resJson.role.should.equal('customer');
277289
resJson.projectId.should.equal(project2.id);
@@ -292,9 +304,11 @@ describe('Project Member Invite create', () => {
292304
result: {
293305
success: true,
294306
status: 200,
295-
content: [{
296-
roleName: USER_ROLE.COPILOT,
297-
}],
307+
content: {
308+
success: [{
309+
roleName: USER_ROLE.COPILOT,
310+
}],
311+
},
298312
},
299313
},
300314
}),
@@ -322,7 +336,7 @@ describe('Project Member Invite create', () => {
322336
if (err) {
323337
done(err);
324338
} else {
325-
const resJson = res.body.result.content[0];
339+
const resJson = res.body.result.content.success[0];
326340
should.exist(resJson);
327341
resJson.role.should.equal('customer');
328342
resJson.projectId.should.equal(project2.id);
@@ -344,9 +358,11 @@ describe('Project Member Invite create', () => {
344358
result: {
345359
success: true,
346360
status: 200,
347-
content: [{
348-
roleName: USER_ROLE.COPILOT,
349-
}],
361+
content: {
362+
success: [{
363+
roleName: USER_ROLE.COPILOT,
364+
}],
365+
},
350366
},
351367
},
352368
}),
@@ -369,7 +385,7 @@ describe('Project Member Invite create', () => {
369385
if (err) {
370386
done(err);
371387
} else {
372-
const resJson = res.body.result.content[0];
388+
const resJson = res.body.result.content.success[0];
373389
should.exist(resJson);
374390
resJson.role.should.equal('customer');
375391
resJson.projectId.should.equal(project2.id);
@@ -390,9 +406,11 @@ describe('Project Member Invite create', () => {
390406
result: {
391407
success: true,
392408
status: 200,
393-
content: [{
394-
roleName: USER_ROLE.COPILOT,
395-
}],
409+
content: {
410+
success: [{
411+
roleName: USER_ROLE.COPILOT,
412+
}],
413+
},
396414
},
397415
},
398416
}),
@@ -415,7 +433,7 @@ describe('Project Member Invite create', () => {
415433
if (err) {
416434
done(err);
417435
} else {
418-
const resJson = res.body.result.content;
436+
const resJson = res.body.result.content.success;
419437
should.exist(resJson);
420438
resJson.length.should.equal(0);
421439
server.services.pubsub.publish.neverCalledWith('project.member.invite.created').should.be.true;
@@ -484,16 +502,18 @@ describe('Project Member Invite create', () => {
484502
it('should return 201 if try to create manager with MANAGER_ROLES', (done) => {
485503
const mockHttpClient = _.merge(testUtil.mockHttpClient, {
486504
get: () => Promise.resolve({
487-
status: 200,
505+
status: 403,
488506
data: {
489507
id: 'requesterId',
490508
version: 'v3',
491509
result: {
492510
success: true,
493-
status: 200,
494-
content: [{
495-
roleName: USER_ROLE.MANAGER,
496-
}],
511+
status: 403,
512+
content: {
513+
failed: [{
514+
message: 'cannot be added with a Manager role to the project',
515+
}],
516+
},
497517
},
498518
},
499519
}),
@@ -511,16 +531,57 @@ describe('Project Member Invite create', () => {
511531
},
512532
})
513533
.expect('Content-Type', /json/)
534+
.expect(403)
535+
.end((err, res) => {
536+
const failed = res.body.result.content.failed[0];
537+
should.exist(failed);
538+
failed.message.should.equal('cannot be added with a Manager role to the project');
539+
done();
540+
});
541+
});
542+
543+
it('should return 201 if try to create customer with COPILOT', (done) => {
544+
const mockHttpClient = _.merge(testUtil.mockHttpClient, {
545+
get: () => Promise.resolve({
546+
status: 200,
547+
data: {
548+
id: 'requesterId',
549+
version: 'v3',
550+
result: {
551+
success: true,
552+
status: 200,
553+
content: {
554+
success: [{
555+
roleName: USER_ROLE.COPILOT,
556+
}],
557+
},
558+
},
559+
},
560+
}),
561+
});
562+
sandbox.stub(util, 'getHttpClient', () => mockHttpClient);
563+
request(server)
564+
.post(`/v4/projects/${project1.id}/members/invite`)
565+
.set({
566+
Authorization: `Bearer ${testUtil.jwts.manager}`,
567+
})
568+
.send({
569+
param: {
570+
userIds: [40051331],
571+
role: 'copilot',
572+
},
573+
})
574+
.expect('Content-Type', /json/)
514575
.expect(201)
515576
.end((err, res) => {
516577
if (err) {
517578
done(err);
518579
} else {
519-
const resJson = res.body.result.content[0];
580+
const resJson = res.body.result.content.success[0];
520581
should.exist(resJson);
521-
resJson.role.should.equal('manager');
582+
resJson.role.should.equal('copilot');
522583
resJson.projectId.should.equal(project1.id);
523-
resJson.userId.should.equal(40152855);
584+
resJson.userId.should.equal(40051331);
524585
server.services.pubsub.publish.calledWith('project.member.invite.created').should.be.true;
525586
done();
526587
}

0 commit comments

Comments
 (0)