diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee index 3f8cbbf5b8..216e2f8a01 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee @@ -20,27 +20,6 @@ module.exports = CollaboratorsInviteController = return next(err) res.json({invites: invites}) - _trySendInviteNotification: (projectId, sendingUser, invite, callback=(err)->) -> - email = invite.email - UserGetter.getUser {email: email}, {_id: 1}, (err, existingUser) -> - if err? - logger.err {projectId, email}, "error checking if user exists" - return callback(err) - if !existingUser? - logger.log {projectId, email}, "no existing user found, returning" - return callback(null) - ProjectGetter.getProject projectId, {_id: 1, name: 1}, (err, project) -> - if err? - logger.err {projectId, email}, "error getting project" - return callback(err) - if !project? - logger.log {projectId}, "no project found while sending notification, returning" - return callback(null) - NotificationsBuilder.projectInvite(invite, project, sendingUser, existingUser).create(callback) - - _tryCancelInviteNotification: (inviteId, currentUser, callback=()->) -> - NotificationsBuilder.projectInvite({_id: inviteId}, null, null, currentUser).read(callback) - inviteToProject: (req, res, next) -> projectId = req.params.Project_id email = req.body.email @@ -57,14 +36,12 @@ module.exports = CollaboratorsInviteController = if !email? or email == "" logger.log {projectId, email, sendingUserId}, "invalid email address" return res.sendStatus(400) - CollaboratorsInviteHandler.inviteToProject projectId, sendingUserId, email, privileges, (err, invite) -> + CollaboratorsInviteHandler.inviteToProject projectId, sendingUser, email, privileges, (err, invite) -> if err? logger.err {projectId, email, sendingUserId}, "error creating project invite" return next(err) logger.log {projectId, email, sendingUserId}, "invite created" - EditorRealTimeController.emitToRoom projectId, 'project:membership:changed', {invites: true} - # async check if email is for an existing user, send a notification - CollaboratorsInviteController._trySendInviteNotification(projectId, sendingUser, invite, ()->) + EditorRealTimeController.emitToRoom(projectId, 'project:membership:changed', {invites: true}) return res.json {invite: invite} revokeInvite: (req, res, next) -> @@ -81,8 +58,9 @@ module.exports = CollaboratorsInviteController = resendInvite: (req, res, next) -> projectId = req.params.Project_id inviteId = req.params.invite_id + sendingUser = req.session.user logger.log {projectId, inviteId}, "resending invite" - CollaboratorsInviteHandler.resendInvite projectId, inviteId, (err) -> + CollaboratorsInviteHandler.resendInvite projectId, sendingUser, inviteId, (err) -> if err? logger.err {projectId, inviteId}, "error resending invite" return next(err) @@ -142,5 +120,4 @@ module.exports = CollaboratorsInviteController = logger.err {projectId, inviteId}, "error accepting invite by token" return next(err) EditorRealTimeController.emitToRoom projectId, 'project:membership:changed', {invites: true, members: true} - CollaboratorsInviteController._tryCancelInviteNotification inviteId, currentUser, () -> res.redirect "/project/#{projectId}" diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee index d57087997d..bf03fe242d 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee @@ -2,10 +2,13 @@ ProjectInvite = require("../../models/ProjectInvite").ProjectInvite logger = require('logger-sharelatex') CollaboratorsEmailHandler = require "./CollaboratorsEmailHandler" CollaboratorsHandler = require "./CollaboratorsHandler" +UserGetter = require "../User/UserGetter" +ProjectGetter = require "../Project/ProjectGetter" Async = require "async" PrivilegeLevels = require "../Authorization/PrivilegeLevels" Errors = require "../Errors/Errors" Crypto = require 'crypto' +NotificationsBuilder = require("../Notifications/NotificationsBuilder") module.exports = CollaboratorsInviteHandler = @@ -27,26 +30,57 @@ module.exports = CollaboratorsInviteHandler = return callback(err) callback(null, count) - inviteToProject: (projectId, sendingUserId, email, privileges, callback=(err,invite)->) -> - logger.log {projectId, sendingUserId, email, privileges}, "adding invite" + _trySendInviteNotification: (projectId, sendingUser, invite, callback=(err)->) -> + email = invite.email + UserGetter.getUser {email: email}, {_id: 1}, (err, existingUser) -> + if err? + logger.err {projectId, email}, "error checking if user exists" + return callback(err) + if !existingUser? + logger.log {projectId, email}, "no existing user found, returning" + return callback(null) + ProjectGetter.getProject projectId, {_id: 1, name: 1}, (err, project) -> + if err? + logger.err {projectId, email}, "error getting project" + return callback(err) + if !project? + logger.log {projectId}, "no project found while sending notification, returning" + return callback(null) + NotificationsBuilder.projectInvite(invite, project, sendingUser, existingUser).create(callback) + + _tryCancelInviteNotification: (inviteId, callback=()->) -> + NotificationsBuilder.projectInvite({_id: inviteId}, null, null, null).read(callback) + + _sendMessages: (projectId, sendingUser, invite, callback=(err)->) -> + logger.log {projectId, inviteId: invite._id}, "sending notification and email for invite" + CollaboratorsEmailHandler.notifyUserOfProjectInvite projectId, invite.email, invite, (err)-> + return callback(err) if err? + CollaboratorsInviteHandler._trySendInviteNotification projectId, sendingUser, invite, (err)-> + return callback(err) if err? + callback() + + inviteToProject: (projectId, sendingUser, email, privileges, callback=(err,invite)->) -> + logger.log {projectId, sendingUserId: sendingUser._id, email, privileges}, "adding invite" Crypto.randomBytes 24, (err, buffer) -> if err? - logger.err {err, projectId, sendingUserId, email}, "error generating random token" + logger.err {err, projectId, sendingUserId: sendingUser._id, email}, "error generating random token" return callback(err) token = buffer.toString('hex') invite = new ProjectInvite { email: email token: token - sendingUserId: sendingUserId + sendingUserId: sendingUser._id projectId: projectId privileges: privileges } invite.save (err, invite) -> if err? - logger.err {err, projectId, sendingUserId, email}, "error saving token" + logger.err {err, projectId, sendingUserId: sendingUser._id, email}, "error saving token" return callback(err) - CollaboratorsEmailHandler.notifyUserOfProjectInvite projectId, email, invite - callback(null, invite) + CollaboratorsInviteHandler._sendMessages projectId, sendingUser, invite, (err) -> + if err? + logger.err {projectId, email}, "error sending messages for invite" + callback(err, invite) revokeInvite: (projectId, inviteId, callback=(err)->) -> logger.log {projectId, inviteId}, "removing invite" @@ -54,9 +88,10 @@ module.exports = CollaboratorsInviteHandler = if err? logger.err {err, projectId, inviteId}, "error removing invite" return callback(err) + CollaboratorsInviteHandler._tryCancelInviteNotification(inviteId, ()->) callback(null) - resendInvite: (projectId, inviteId, callback=(err)->) -> + resendInvite: (projectId, sendingUser, inviteId, callback=(err)->) -> logger.log {projectId, inviteId}, "resending invite email" ProjectInvite.findOne {_id: inviteId, projectId: projectId}, (err, invite) -> if err? @@ -65,8 +100,11 @@ module.exports = CollaboratorsInviteHandler = if !invite? logger.err {err, projectId, inviteId}, "no invite found, nothing to resend" return callback(null) - CollaboratorsEmailHandler.notifyUserOfProjectInvite projectId, invite.email, invite - callback(null) + CollaboratorsInviteHandler._sendMessages projectId, sendingUser, invite, (err) -> + if err? + logger.err {projectid, inviteId}, "error resending invite messages" + return callback(err) + callback(null) getInviteByToken: (projectId, tokenString, callback=(err,invite)->) -> logger.log {projectId, tokenString}, "fetching invite by token" @@ -100,4 +138,5 @@ module.exports = CollaboratorsInviteHandler = if err? logger.err {err, projectId, inviteId}, "error removing invite" return callback(err) + CollaboratorsInviteHandler._tryCancelInviteNotification inviteId, ()-> callback() diff --git a/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee b/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee index ffb71f70e6..646a520a1c 100644 --- a/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee +++ b/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee @@ -12,7 +12,7 @@ module.exports = groupName: licence.name subscription_id: licence.subscription_id logger.log user_id:user._id, key:key, "creating notification key for user" - NotificationsHandler.createNotification user._id, @key, "notification_group_invite", messageOpts, null, callback + NotificationsHandler.createNotification user._id, @key, "notification_group_invite", messageOpts, null, false, callback read: (callback = ->)-> NotificationsHandler.markAsReadWithKey user._id, @key, callback @@ -26,6 +26,6 @@ module.exports = projectId: project._id.toString() token: invite.token logger.log {user_id: user._id, project_id: project._id, invite_id: invite._id, key: @key}, "creating project invite notification for user" - NotificationsHandler.createNotification user._id, @key, "notification_project_invite", messageOpts, invite.expires, callback + NotificationsHandler.createNotification user._id, @key, "notification_project_invite", messageOpts, invite.expires, true, callback read: (callback=()->) -> NotificationsHandler.markAsReadByKeyOnly @key, callback diff --git a/services/web/app/coffee/Features/Notifications/NotificationsHandler.coffee b/services/web/app/coffee/Features/Notifications/NotificationsHandler.coffee index 9fc21659c9..2ee97dae49 100644 --- a/services/web/app/coffee/Features/Notifications/NotificationsHandler.coffee +++ b/services/web/app/coffee/Features/Notifications/NotificationsHandler.coffee @@ -29,7 +29,7 @@ module.exports = unreadNotifications = [] callback(null, unreadNotifications) - createNotification: (user_id, key, templateKey, messageOpts, expiryDateTime, callback)-> + createNotification: (user_id, key, templateKey, messageOpts, expiryDateTime, forceCreate, callback)-> payload = { key:key messageOpts:messageOpts @@ -37,6 +37,8 @@ module.exports = } if expiryDateTime? payload.expires = expiryDateTime + if forceCreate + payload.forceCreate = true opts = uri: "#{settings.apis.notifications?.url}/user/#{user_id}" timeout: oneSecond diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee index 6ad001f466..346ac3a3c1 100644 --- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee @@ -26,166 +26,6 @@ describe "CollaboratorsInviteController", -> @project_id = "project-id-123" @callback = sinon.stub() - describe '_tryCancelInviteNotification', -> - beforeEach -> - @inviteId = ObjectId() - @currentUser = {_id: ObjectId()} - @notification = {read: sinon.stub().callsArgWith(0, null)} - @NotificationsBuilder.projectInvite = sinon.stub().returns(@notification) - @call = (callback) => - @CollaboratorsInviteController._tryCancelInviteNotification @inviteId, @currentUser, callback - - it 'should not produce an error', (done) -> - @call (err) => - expect(err).to.be.oneOf [null, undefined] - done() - - it 'should call notification.read', (done) -> - @call (err) => - @notification.read.callCount.should.equal 1 - done() - - describe 'when notification.read produces an error', -> - beforeEach -> - @notification = {read: sinon.stub().callsArgWith(0, new Error('woops'))} - @NotificationsBuilder.projectInvite = sinon.stub().returns(@notification) - - it 'should produce an error', (done) -> - @call (err) => - expect(err).to.be.instanceof Error - done() - - describe "_trySendInviteNotification", -> - - beforeEach -> - @invite = - _id: ObjectId(), - token: "some_token", - sendingUserId: ObjectId(), - projectId: @project_id, - targetEmail: 'user@example.com' - createdAt: new Date(), - @sendingUser = - _id: ObjectId() - first_name: "jim" - @existingUser = {_id: ObjectId()} - @UserGetter.getUser = sinon.stub().callsArgWith(2, null, @existingUser) - @fakeProject = - _id: @project_id - name: "some project" - @ProjectGetter.getProject = sinon.stub().callsArgWith(2, null, @fakeProject) - @notification = {create: sinon.stub().callsArgWith(0, null)} - @NotificationsBuilder.projectInvite = sinon.stub().returns(@notification) - @call = (callback) => - @CollaboratorsInviteController._trySendInviteNotification @project_id, @sendingUser, @invite, callback - - describe 'when the user exists', -> - - beforeEach -> - - it 'should not produce an error', (done) -> - @call (err) => - expect(err).to.be.oneOf [null, undefined] - done() - - it 'should call getUser', (done) -> - @call (err) => - @UserGetter.getUser.callCount.should.equal 1 - @UserGetter.getUser.calledWith({email: @invite.email}).should.equal true - done() - - it 'should call getProject', (done) -> - @call (err) => - @ProjectGetter.getProject.callCount.should.equal 1 - @ProjectGetter.getProject.calledWith(@project_id).should.equal true - done() - - it 'should call NotificationsBuilder.projectInvite.create', (done) -> - @call (err) => - @NotificationsBuilder.projectInvite.callCount.should.equal 1 - @notification.create.callCount.should.equal 1 - done() - - describe 'when getProject produces an error', -> - - beforeEach -> - @ProjectGetter.getProject.callsArgWith(2, new Error('woops')) - - it 'should produce an error', (done) -> - @call (err) => - expect(err).to.be.instanceof Error - done() - - it 'should not call NotificationsBuilder.projectInvite.create', (done) -> - @call (err) => - @NotificationsBuilder.projectInvite.callCount.should.equal 0 - @notification.create.callCount.should.equal 0 - done() - - describe 'when projectInvite.create produces an error', -> - - beforeEach -> - @notification.create.callsArgWith(0, new Error('woops')) - - it 'should produce an error', (done) -> - @call (err) => - expect(err).to.be.instanceof Error - done() - - describe 'when the user does not exist', -> - - beforeEach -> - @UserGetter.getUser = sinon.stub().callsArgWith(2, null, null) - - it 'should not produce an error', (done) -> - @call (err) => - expect(err).to.be.oneOf [null, undefined] - done() - - it 'should call getUser', (done) -> - @call (err) => - @UserGetter.getUser.callCount.should.equal 1 - @UserGetter.getUser.calledWith({email: @invite.email}).should.equal true - done() - - it 'should not call getProject', (done) -> - @call (err) => - @ProjectGetter.getProject.callCount.should.equal 0 - done() - - it 'should not call NotificationsBuilder.projectInvite.create', (done) -> - @call (err) => - @NotificationsBuilder.projectInvite.callCount.should.equal 0 - @notification.create.callCount.should.equal 0 - done() - - describe 'when the getUser produces an error', -> - - beforeEach -> - @UserGetter.getUser = sinon.stub().callsArgWith(2, new Error('woops')) - - it 'should produce an error', (done) -> - @call (err) => - expect(err).to.be.instanceof Error - done() - - it 'should call getUser', (done) -> - @call (err) => - @UserGetter.getUser.callCount.should.equal 1 - @UserGetter.getUser.calledWith({email: @invite.email}).should.equal true - done() - - it 'should not call getProject', (done) -> - @call (err) => - @ProjectGetter.getProject.callCount.should.equal 0 - done() - - it 'should not call NotificationsBuilder.projectInvite.create', (done) -> - @call (err) => - @NotificationsBuilder.projectInvite.callCount.should.equal 0 - @notification.create.callCount.should.equal 0 - done() - describe 'getAllInvites', -> beforeEach -> @@ -231,8 +71,10 @@ describe "CollaboratorsInviteController", -> @targetEmail = "user@example.com" @req.params = Project_id: @project_id + @current_user = + _id: @current_user_id = "current-user-id" @req.session = - user: _id: @current_user_id = "current-user-id" + user: @current_user @req.body = email: @targetEmail privileges: @privileges = "readAndWrite" @@ -248,7 +90,6 @@ describe "CollaboratorsInviteController", -> } @LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, null, true) @CollaboratorsInviteHandler.inviteToProject = sinon.stub().callsArgWith(4, null, @invite) - @CollaboratorsInviteController._trySendInviteNotification = sinon.stub() @callback = sinon.stub() @next = sinon.stub() @@ -268,15 +109,12 @@ describe "CollaboratorsInviteController", -> it 'should have called inviteToProject', -> @CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 1 - @CollaboratorsInviteHandler.inviteToProject.calledWith(@project_id,@current_user_id,@targetEmail,@privileges).should.equal true + @CollaboratorsInviteHandler.inviteToProject.calledWith(@project_id,@current_user,@targetEmail,@privileges).should.equal true it 'should have called emitToRoom', -> @EditorRealTimeController.emitToRoom.callCount.should.equal 1 @EditorRealTimeController.emitToRoom.calledWith(@project_id, 'project:membership:changed').should.equal true - it 'should call _trySendInviteNotification', -> - @CollaboratorsInviteController._trySendInviteNotification.callCount.should.equal 1 - describe 'when the user is not allowed to add more collaborators', -> beforeEach -> @@ -321,7 +159,7 @@ describe "CollaboratorsInviteController", -> it 'should have called inviteToProject', -> @CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 1 - @CollaboratorsInviteHandler.inviteToProject.calledWith(@project_id,@current_user_id,@targetEmail,@privileges).should.equal true + @CollaboratorsInviteHandler.inviteToProject.calledWith(@project_id,@current_user,@targetEmail,@privileges).should.equal true describe "viewInvite", -> @@ -604,7 +442,7 @@ describe "CollaboratorsInviteController", -> user: _id: @current_user_id = "current-user-id" @res.render = sinon.stub() @res.sendStatus = sinon.stub() - @CollaboratorsInviteHandler.resendInvite = sinon.stub().callsArgWith(2, null) + @CollaboratorsInviteHandler.resendInvite = sinon.stub().callsArgWith(3, null) @callback = sinon.stub() @next = sinon.stub() @@ -624,7 +462,7 @@ describe "CollaboratorsInviteController", -> beforeEach -> @err = new Error('woops') - @CollaboratorsInviteHandler.resendInvite = sinon.stub().callsArgWith(2, @err) + @CollaboratorsInviteHandler.resendInvite = sinon.stub().callsArgWith(3, @err) @CollaboratorsInviteController.resendInvite @req, @res, @next it 'should not produce a 201 response', -> @@ -637,15 +475,16 @@ describe "CollaboratorsInviteController", -> it 'should have called resendInvite', -> @CollaboratorsInviteHandler.resendInvite.callCount.should.equal 1 - describe "revokeInvite", -> beforeEach -> @req.params = Project_id: @project_id invite_id: @invite_id = "thuseoautoh" + @current_user = + _id: @current_user_id = "current-user-id" @req.session = - user: _id: @current_user_id = "current-user-id" + user: @current_user @res.render = sinon.stub() @res.sendStatus = sinon.stub() @CollaboratorsInviteHandler.revokeInvite = sinon.stub().callsArgWith(2, null) @@ -698,7 +537,6 @@ describe "CollaboratorsInviteController", -> @res.render = sinon.stub() @res.redirect = sinon.stub() @CollaboratorsInviteHandler.acceptInvite = sinon.stub().callsArgWith(4, null) - @CollaboratorsInviteController._tryCancelInviteNotification = sinon.stub() @callback = sinon.stub() @next = sinon.stub() @@ -718,9 +556,6 @@ describe "CollaboratorsInviteController", -> @EditorRealTimeController.emitToRoom.callCount.should.equal 1 @EditorRealTimeController.emitToRoom.calledWith(@project_id, 'project:membership:changed').should.equal true - it 'should call _tryCancelInviteNotification', -> - @CollaboratorsInviteController._tryCancelInviteNotification.callCount.should.equal 1 - describe 'when revokeInvite produces an error', -> beforeEach -> diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee index 0e8ceef0b3..d4ae9a4229 100644 --- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee +++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee @@ -24,14 +24,20 @@ describe "CollaboratorsInviteHandler", -> @Crypto = Crypto @CollaboratorsInviteHandler = SandboxedModule.require modulePath, requires: 'settings-sharelatex': @settings = {} + '../../models/ProjectInvite': {ProjectInvite: @ProjectInvite} 'logger-sharelatex': @logger = {err: sinon.stub(), error: sinon.stub(), log: sinon.stub()} './CollaboratorsEmailHandler': @CollaboratorsEmailHandler = {} "./CollaboratorsHandler": @CollaboratorsHandler = {addUserIdToProject: sinon.stub()} - '../../models/ProjectInvite': {ProjectInvite: @ProjectInvite} + '../User/UserGetter': @UserGetter = {getUser: sinon.stub()} + "../Project/ProjectGetter": @ProjectGetter = {} + "../Notifications/NotificationsBuilder": @NotificationsBuilder = {} 'crypto': @Crypto @projectId = ObjectId() @sendingUserId = ObjectId() + @sendingUser = + _id: @sendingUserId + name: "Bob" @email = "user@example.com" @userId = ObjectId() @user = @@ -125,9 +131,9 @@ describe "CollaboratorsInviteHandler", -> beforeEach -> @ProjectInvite::save = sinon.spy (cb) -> cb(null, this) @randomBytesSpy = sinon.spy(@Crypto, 'randomBytes') - @CollaboratorsEmailHandler.notifyUserOfProjectInvite = sinon.stub() + @CollaboratorsInviteHandler._sendMessages = sinon.stub().callsArgWith(3, null) @call = (callback) => - @CollaboratorsInviteHandler.inviteToProject @projectId, @sendingUserId, @email, @privileges, callback + @CollaboratorsInviteHandler.inviteToProject @projectId, @sendingUser, @email, @privileges, callback afterEach -> @randomBytesSpy.restore() @@ -160,10 +166,10 @@ describe "CollaboratorsInviteHandler", -> @ProjectInvite::save.callCount.should.equal 1 done() - it 'should have called CollaboratorsEmailHandler.notifyUserOfProjectInvite', (done) -> + it 'should have called _sendMessages', (done) -> @call (err, invite) => - @CollaboratorsEmailHandler.notifyUserOfProjectInvite.callCount.should.equal 1 - @CollaboratorsEmailHandler.notifyUserOfProjectInvite.calledWith(@projectId, @email).should.equal true + @CollaboratorsInviteHandler._sendMessages.callCount.should.equal 1 + @CollaboratorsInviteHandler._sendMessages.calledWith(@projectId, @sendingUser).should.equal true done() describe 'when saving model produces an error', -> @@ -176,10 +182,64 @@ describe "CollaboratorsInviteHandler", -> expect(err).to.be.instanceof Error done() + describe '_sendMessages', -> + + beforeEach -> + @CollaboratorsEmailHandler.notifyUserOfProjectInvite = sinon.stub().callsArgWith(3, null) + @CollaboratorsInviteHandler._trySendInviteNotification = sinon.stub().callsArgWith(3, null) + @call = (callback) => + @CollaboratorsInviteHandler._sendMessages @projectId, @sendingUser, @fakeInvite, callback + + describe 'when all goes well', -> + + it 'should not produce an error', (done) -> + @call (err) => + expect(err).to.not.be.instanceof Error + expect(err).to.be.oneOf [null, undefined] + done() + + it 'should call CollaboratorsEmailHandler.notifyUserOfProjectInvite', (done) -> + @call (err) => + @CollaboratorsEmailHandler.notifyUserOfProjectInvite.callCount.should.equal 1 + @CollaboratorsEmailHandler.notifyUserOfProjectInvite.calledWith(@projectId, @fakeInvite.email, @fakeInvite).should.equal true + done() + + it 'should call _trySendInviteNotification', (done) -> + @call (err) => + @CollaboratorsInviteHandler._trySendInviteNotification.callCount.should.equal 1 + @CollaboratorsInviteHandler._trySendInviteNotification.calledWith(@projectId, @sendingUser, @fakeInvite).should.equal true + done() + + describe 'when CollaboratorsEmailHandler.notifyUserOfProjectInvite produces an error', -> + + beforeEach -> + @CollaboratorsEmailHandler.notifyUserOfProjectInvite = sinon.stub().callsArgWith(3, new Error('woops')) + + it 'should produce an error', (done) -> + @call (err, invite) => + expect(err).to.be.instanceof Error + done() + + it 'should not call _trySendInviteNotification', (done) -> + @call (err) => + @CollaboratorsInviteHandler._trySendInviteNotification.callCount.should.equal 0 + done() + + describe 'when _trySendInviteNotification produces an error', -> + + beforeEach -> + @CollaboratorsInviteHandler._trySendInviteNotification = sinon.stub().callsArgWith(3, new Error('woops')) + + it 'should produce an error', (done) -> + @call (err, invite) => + expect(err).to.be.instanceof Error + done() + describe 'revokeInvite', -> beforeEach -> @ProjectInvite.remove.callsArgWith(1, null) + @CollaboratorsInviteHandler._tryCancelInviteNotification = sinon.stub().callsArgWith(1, null) @call = (callback) => @CollaboratorsInviteHandler.revokeInvite @projectId, @inviteId, callback @@ -199,6 +259,12 @@ describe "CollaboratorsInviteHandler", -> @ProjectInvite.remove.calledWith({projectId: @projectId, _id: @inviteId}).should.equal true done() + it 'should call _tryCancelInviteNotification', (done) -> + @call (err) => + @CollaboratorsInviteHandler._tryCancelInviteNotification.callCount.should.equal 1 + @CollaboratorsInviteHandler._tryCancelInviteNotification.calledWith(@inviteId).should.equal true + done() + describe 'when remove produces an error', -> beforeEach -> @@ -213,9 +279,9 @@ describe "CollaboratorsInviteHandler", -> beforeEach -> @ProjectInvite.findOne.callsArgWith(1, null, @fakeInvite) - @CollaboratorsEmailHandler.notifyUserOfProjectInvite = sinon.stub() + @CollaboratorsInviteHandler._sendMessages = sinon.stub().callsArgWith(3, null) @call = (callback) => - @CollaboratorsInviteHandler.resendInvite @projectId, @inviteId, callback + @CollaboratorsInviteHandler.resendInvite @projectId, @sendingUser, @inviteId, callback describe 'when all goes well', -> @@ -233,10 +299,10 @@ describe "CollaboratorsInviteHandler", -> @ProjectInvite.findOne.calledWith({_id: @inviteId, projectId: @projectId}).should.equal true done() - it 'should have called CollaboratorsEmailHandler.notifyUserOfProjectInvite', (done) -> + it 'should have called _sendMessages', (done) -> @call (err, invite) => - @CollaboratorsEmailHandler.notifyUserOfProjectInvite.callCount.should.equal 1 - @CollaboratorsEmailHandler.notifyUserOfProjectInvite.calledWith(@projectId, @email).should.equal true + @CollaboratorsInviteHandler._sendMessages.callCount.should.equal 1 + @CollaboratorsInviteHandler._sendMessages.calledWith(@projectId, @sendingUser, @fakeInvite).should.equal true done() describe 'when findOne produces an error', -> @@ -249,9 +315,9 @@ describe "CollaboratorsInviteHandler", -> expect(err).to.be.instanceof Error done() - it 'should not have called CollaboratorsEmailHandler.notifyUserOfProjectInvite', (done) -> + it 'should not have called _sendMessages', (done) -> @call (err, invite) => - @CollaboratorsEmailHandler.notifyUserOfProjectInvite.callCount.should.equal 0 + @CollaboratorsInviteHandler._sendMessages.callCount.should.equal 0 done() describe 'when findOne does not find an invite', -> @@ -265,9 +331,9 @@ describe "CollaboratorsInviteHandler", -> expect(err).to.be.oneOf [null, undefined] done() - it 'should not have called CollaboratorsEmailHandler.notifyUserOfProjectInvite', (done) -> + it 'should not have called _sendMessages', (done) -> @call (err, invite) => - @CollaboratorsEmailHandler.notifyUserOfProjectInvite.callCount.should.equal 0 + @CollaboratorsInviteHandler._sendMessages.callCount.should.equal 0 done() describe 'getInviteByToken', -> @@ -335,6 +401,7 @@ describe "CollaboratorsInviteHandler", -> @CollaboratorsHandler.addUserIdToProject.callsArgWith(4, null) @_getInviteByToken = sinon.stub(@CollaboratorsInviteHandler, 'getInviteByToken') @_getInviteByToken.callsArgWith(2, null, @fakeInvite) + @CollaboratorsInviteHandler._tryCancelInviteNotification = sinon.stub().callsArgWith(1, null) @ProjectInvite.remove.callsArgWith(1, null) @call = (callback) => @CollaboratorsInviteHandler.acceptInvite @projectId, @inviteId, @token, @user, callback @@ -494,3 +561,163 @@ describe "CollaboratorsInviteHandler", -> @call (err) => @ProjectInvite.remove.callCount.should.equal 1 done() + + describe '_tryCancelInviteNotification', -> + beforeEach -> + @inviteId = ObjectId() + @currentUser = {_id: ObjectId()} + @notification = {read: sinon.stub().callsArgWith(0, null)} + @NotificationsBuilder.projectInvite = sinon.stub().returns(@notification) + @call = (callback) => + @CollaboratorsInviteHandler._tryCancelInviteNotification @inviteId, callback + + it 'should not produce an error', (done) -> + @call (err) => + expect(err).to.be.oneOf [null, undefined] + done() + + it 'should call notification.read', (done) -> + @call (err) => + @notification.read.callCount.should.equal 1 + done() + + describe 'when notification.read produces an error', -> + beforeEach -> + @notification = {read: sinon.stub().callsArgWith(0, new Error('woops'))} + @NotificationsBuilder.projectInvite = sinon.stub().returns(@notification) + + it 'should produce an error', (done) -> + @call (err) => + expect(err).to.be.instanceof Error + done() + + describe "_trySendInviteNotification", -> + + beforeEach -> + @invite = + _id: ObjectId(), + token: "some_token", + sendingUserId: ObjectId(), + projectId: @project_id, + targetEmail: 'user@example.com' + createdAt: new Date(), + @sendingUser = + _id: ObjectId() + first_name: "jim" + @existingUser = {_id: ObjectId()} + @UserGetter.getUser = sinon.stub().callsArgWith(2, null, @existingUser) + @fakeProject = + _id: @project_id + name: "some project" + @ProjectGetter.getProject = sinon.stub().callsArgWith(2, null, @fakeProject) + @notification = {create: sinon.stub().callsArgWith(0, null)} + @NotificationsBuilder.projectInvite = sinon.stub().returns(@notification) + @call = (callback) => + @CollaboratorsInviteHandler._trySendInviteNotification @project_id, @sendingUser, @invite, callback + + describe 'when the user exists', -> + + beforeEach -> + + it 'should not produce an error', (done) -> + @call (err) => + expect(err).to.be.oneOf [null, undefined] + done() + + it 'should call getUser', (done) -> + @call (err) => + @UserGetter.getUser.callCount.should.equal 1 + @UserGetter.getUser.calledWith({email: @invite.email}).should.equal true + done() + + it 'should call getProject', (done) -> + @call (err) => + @ProjectGetter.getProject.callCount.should.equal 1 + @ProjectGetter.getProject.calledWith(@project_id).should.equal true + done() + + it 'should call NotificationsBuilder.projectInvite.create', (done) -> + @call (err) => + @NotificationsBuilder.projectInvite.callCount.should.equal 1 + @notification.create.callCount.should.equal 1 + done() + + describe 'when getProject produces an error', -> + + beforeEach -> + @ProjectGetter.getProject.callsArgWith(2, new Error('woops')) + + it 'should produce an error', (done) -> + @call (err) => + expect(err).to.be.instanceof Error + done() + + it 'should not call NotificationsBuilder.projectInvite.create', (done) -> + @call (err) => + @NotificationsBuilder.projectInvite.callCount.should.equal 0 + @notification.create.callCount.should.equal 0 + done() + + describe 'when projectInvite.create produces an error', -> + + beforeEach -> + @notification.create.callsArgWith(0, new Error('woops')) + + it 'should produce an error', (done) -> + @call (err) => + expect(err).to.be.instanceof Error + done() + + describe 'when the user does not exist', -> + + beforeEach -> + @UserGetter.getUser = sinon.stub().callsArgWith(2, null, null) + + it 'should not produce an error', (done) -> + @call (err) => + expect(err).to.be.oneOf [null, undefined] + done() + + it 'should call getUser', (done) -> + @call (err) => + @UserGetter.getUser.callCount.should.equal 1 + @UserGetter.getUser.calledWith({email: @invite.email}).should.equal true + done() + + it 'should not call getProject', (done) -> + @call (err) => + @ProjectGetter.getProject.callCount.should.equal 0 + done() + + it 'should not call NotificationsBuilder.projectInvite.create', (done) -> + @call (err) => + @NotificationsBuilder.projectInvite.callCount.should.equal 0 + @notification.create.callCount.should.equal 0 + done() + + describe 'when the getUser produces an error', -> + + beforeEach -> + @UserGetter.getUser = sinon.stub().callsArgWith(2, new Error('woops')) + + it 'should produce an error', (done) -> + @call (err) => + expect(err).to.be.instanceof Error + done() + + it 'should call getUser', (done) -> + @call (err) => + @UserGetter.getUser.callCount.should.equal 1 + @UserGetter.getUser.calledWith({email: @invite.email}).should.equal true + done() + + it 'should not call getProject', (done) -> + @call (err) => + @ProjectGetter.getProject.callCount.should.equal 0 + done() + + it 'should not call NotificationsBuilder.projectInvite.create', (done) -> + @call (err) => + @NotificationsBuilder.projectInvite.callCount.should.equal 0 + @notification.create.callCount.should.equal 0 + done() diff --git a/services/web/test/UnitTests/coffee/Notifications/NotificationsHandlerTests.coffee b/services/web/test/UnitTests/coffee/Notifications/NotificationsHandlerTests.coffee index 9629bca8c7..0aafe6e5fc 100644 --- a/services/web/test/UnitTests/coffee/Notifications/NotificationsHandlerTests.coffee +++ b/services/web/test/UnitTests/coffee/Notifications/NotificationsHandlerTests.coffee @@ -61,9 +61,10 @@ describe 'NotificationsHandler', -> @messageOpts = {value:12344} @templateKey = "renderThisHtml" @expiry = null + @forceCreate = false it "should post the message over", (done)-> - @handler.createNotification user_id, @key, @templateKey, @messageOpts, @expiry, => + @handler.createNotification user_id, @key, @templateKey, @messageOpts, @expiry, @forceCreate, => args = @request.args[0][0] args.uri.should.equal "#{notificationUrl}/user/#{user_id}" args.timeout.should.equal 1000 @@ -77,9 +78,10 @@ describe 'NotificationsHandler', -> @messageOpts = {value:12344} @templateKey = "renderThisHtml" @expiry = new Date() + @forceCreate = false it 'should post the message over with expiry field', (done) -> - @handler.createNotification user_id, @key, @templateKey, @messageOpts, @expiry, => + @handler.createNotification user_id, @key, @templateKey, @messageOpts, @expiry, @forceCreate, => args = @request.args[0][0] args.uri.should.equal "#{notificationUrl}/user/#{user_id}" args.timeout.should.equal 1000 @@ -87,6 +89,23 @@ describe 'NotificationsHandler', -> assert.deepEqual(args.json, expectedJson) done() + describe 'when forceCreate is true', -> + beforeEach -> + @key = "some key here" + @messageOpts = {value:12344} + @templateKey = "renderThisHtml" + @expiry = null + @forceCreate = true + + it 'should add a forceCreate=true flag to the payload', (done) -> + @handler.createNotification user_id, @key, @templateKey, @messageOpts, @expiry, @forceCreate, => + args = @request.args[0][0] + args.uri.should.equal "#{notificationUrl}/user/#{user_id}" + args.timeout.should.equal 1000 + expectedJson = {key:@key, templateKey:@templateKey, messageOpts:@messageOpts, forceCreate: @forceCreate} + assert.deepEqual(args.json, expectedJson) + done() + describe "markAsReadByKeyOnly", -> beforeEach -> @key = "some key here"