Merge branch 'master' into pr-ab-subscription-form

This commit is contained in:
Paulo Reis
2016-08-23 14:10:19 +01:00
48 changed files with 3079 additions and 221 deletions

View File

@@ -4,7 +4,9 @@ ProjectEditorHandler = require "../Project/ProjectEditorHandler"
EditorRealTimeController = require "../Editor/EditorRealTimeController"
LimitationsManager = require "../Subscription/LimitationsManager"
UserGetter = require "../User/UserGetter"
mimelib = require("mimelib")
EmailHelper = require "../Helpers/EmailHelper"
logger = require 'logger-sharelatex'
module.exports = CollaboratorsController =
addUserToProject: (req, res, next) ->
@@ -16,11 +18,11 @@ module.exports = CollaboratorsController =
return res.json { user: false }
else
{email, privileges} = req.body
email = mimelib.parseAddresses(email or "")[0]?.address?.toLowerCase()
email = EmailHelper.parseEmail(email)
if !email? or email == ""
return res.status(400).send("invalid email address")
adding_user_id = req.session?.user?._id
CollaboratorsHandler.addEmailToProject project_id, adding_user_id, email, privileges, (error, user_id) =>
return next(error) if error?
@@ -35,8 +37,9 @@ module.exports = CollaboratorsController =
user_id = req.params.user_id
CollaboratorsController._removeUserIdFromProject project_id, user_id, (error) ->
return next(error) if error?
EditorRealTimeController.emitToRoom project_id, 'project:membership:changed', {members: true}
res.sendStatus 204
removeSelfFromProject: (req, res, next = (error) ->) ->
project_id = req.params.Project_id
user_id = req.session?.user?._id
@@ -50,3 +53,11 @@ module.exports = CollaboratorsController =
EditorRealTimeController.emitToRoom(project_id, 'userRemovedFromProject', user_id)
callback()
getAllMembers: (req, res, next) ->
projectId = req.params.Project_id
logger.log {projectId}, "getting all active members for project"
CollaboratorsHandler.getAllMembers projectId, (err, members) ->
if err?
logger.err {projectId}, "error getting members for project"
return next(err)
res.json({members: members})

View File

@@ -2,7 +2,15 @@ Project = require("../../models/Project").Project
EmailHandler = require("../Email/EmailHandler")
Settings = require "settings-sharelatex"
module.exports =
module.exports = CollaboratorsEmailHandler =
_buildInviteUrl: (project, invite) ->
"#{Settings.siteUrl}/project/#{project._id}/invite/token/#{invite.token}?" + [
"project_name=#{encodeURIComponent(project.name)}"
"user_first_name=#{encodeURIComponent(project.owner_ref.first_name)}"
].join("&")
notifyUserOfProjectShare: (project_id, email, callback)->
Project
.findOne(_id: project_id )
@@ -22,4 +30,19 @@ module.exports =
"rs=ci" # referral source = collaborator invite
].join("&")
owner: project.owner_ref
EmailHandler.sendEmail "projectSharedWithYou", emailOptions, callback
EmailHandler.sendEmail "projectSharedWithYou", emailOptions, callback
notifyUserOfProjectInvite: (project_id, email, invite, callback)->
Project
.findOne(_id: project_id )
.select("name owner_ref")
.populate('owner_ref')
.exec (err, project)->
emailOptions =
to: email
replyTo: project.owner_ref.email
project:
name: project.name
inviteUrl: CollaboratorsEmailHandler._buildInviteUrl(project, invite)
owner: project.owner_ref
EmailHandler.sendEmail "projectInvite", emailOptions, callback

View File

@@ -1,6 +1,5 @@
UserCreator = require('../User/UserCreator')
Project = require("../../models/Project").Project
mimelib = require("mimelib")
logger = require('logger-sharelatex')
UserGetter = require "../User/UserGetter"
ContactManager = require "../Contacts/ContactManager"
@@ -8,8 +7,12 @@ CollaboratorsEmailHandler = require "./CollaboratorsEmailHandler"
async = require "async"
PrivilegeLevels = require "../Authorization/PrivilegeLevels"
Errors = require "../Errors/Errors"
EmailHelper = require "../Helpers/EmailHelper"
ProjectEditorHandler = require "../Project/ProjectEditorHandler"
module.exports = CollaboratorsHandler =
getMemberIdsWithPrivilegeLevels: (project_id, callback = (error, members) ->) ->
Project.findOne { _id: project_id }, { owner_ref: 1, collaberator_refs: 1, readOnly_refs: 1 }, (error, project) ->
return callback(error) if error?
@@ -21,12 +24,12 @@ module.exports = CollaboratorsHandler =
for member_id in project.collaberator_refs or []
members.push { id: member_id.toString(), privilegeLevel: PrivilegeLevels.READ_AND_WRITE }
return callback null, members
getMemberIds: (project_id, callback = (error, member_ids) ->) ->
CollaboratorsHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members) ->
return callback(error) if error?
return callback null, members.map (m) -> m.id
getMembersWithPrivilegeLevels: (project_id, callback = (error, members) ->) ->
CollaboratorsHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members = []) ->
return callback(error) if error?
@@ -41,7 +44,7 @@ module.exports = CollaboratorsHandler =
(error) ->
return callback(error) if error?
callback null, result
getMemberIdPrivilegeLevel: (user_id, project_id, callback = (error, privilegeLevel) ->) ->
# In future if the schema changes and getting all member ids is more expensive (multiple documents)
# then optimise this.
@@ -51,12 +54,12 @@ module.exports = CollaboratorsHandler =
if member.id == user_id?.toString()
return callback null, member.privilegeLevel
return callback null, PrivilegeLevels.NONE
getMemberCount: (project_id, callback = (error, count) ->) ->
CollaboratorsHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members) ->
return callback(error) if error?
return callback null, (members or []).length
getCollaboratorCount: (project_id, callback = (error, count) ->) ->
CollaboratorsHandler.getMemberCount project_id, (error, count) ->
return callback(error) if error?
@@ -69,14 +72,14 @@ module.exports = CollaboratorsHandler =
if member.id.toString() == user_id.toString()
return callback null, true, member.privilegeLevel
return callback null, false, null
getProjectsUserIsCollaboratorOf: (user_id, fields, callback = (error, readAndWriteProjects, readOnlyProjects) ->) ->
Project.find {collaberator_refs:user_id}, fields, (err, readAndWriteProjects)=>
return callback(err) if err?
Project.find {readOnly_refs:user_id}, fields, (err, readOnlyProjects)=>
return callback(err) if err?
callback(null, readAndWriteProjects, readOnlyProjects)
removeUserFromProject: (project_id, user_id, callback = (error) ->)->
logger.log user_id: user_id, project_id: project_id, "removing user"
conditions = _id:project_id
@@ -86,7 +89,7 @@ module.exports = CollaboratorsHandler =
if err?
logger.error err: err, "problem removing user from project collaberators"
callback(err)
removeUserFromAllProjets: (user_id, callback = (error) ->) ->
CollaboratorsHandler.getProjectsUserIsCollaboratorOf user_id, { _id: 1 }, (error, readAndWriteProjects = [], readOnlyProjects = []) ->
return callback(error) if error?
@@ -98,10 +101,9 @@ module.exports = CollaboratorsHandler =
return cb() if !project?
CollaboratorsHandler.removeUserFromProject project._id, user_id, cb
async.series jobs, callback
addEmailToProject: (project_id, adding_user_id, unparsed_email, privilegeLevel, callback = (error, user) ->) ->
emails = mimelib.parseAddresses(unparsed_email)
email = emails[0]?.address?.toLowerCase()
email = EmailHelper.parseEmail(unparsed_email)
if !email? or email == ""
return callback(new Error("no valid email provided: '#{unparsed_email}'"))
UserCreator.getUserOrCreateHoldingAccount email, (error, user) ->
@@ -118,7 +120,7 @@ module.exports = CollaboratorsHandler =
existing_users = existing_users.map (u) -> u.toString()
if existing_users.indexOf(user_id.toString()) > -1
return callback null # User already in Project
if privilegeLevel == PrivilegeLevels.READ_AND_WRITE
level = {"collaberator_refs":user_id}
logger.log {privileges: "readAndWrite", user_id, project_id}, "adding user"
@@ -128,11 +130,6 @@ module.exports = CollaboratorsHandler =
else
return callback(new Error("unknown privilegeLevel: #{privilegeLevel}"))
# Do these in the background
UserGetter.getUser user_id, {email: 1}, (error, user) ->
if error?
logger.error {err: error, project_id, user_id}, "error getting user while adding to project"
CollaboratorsEmailHandler.notifyUserOfProjectShare project_id, user.email
ContactManager.addContact adding_user_id, user_id
Project.update { _id: project_id }, { $addToSet: level }, (error) ->
@@ -143,3 +140,12 @@ module.exports = CollaboratorsHandler =
if error?
logger.error {err: error, project_id, user_id}, "error flushing to TPDS after adding collaborator"
callback()
getAllMembers: (projectId, callback=(err, members)->) ->
logger.log {projectId}, "fetching all members"
CollaboratorsHandler.getMembersWithPrivilegeLevels projectId, (error, rawMembers) ->
if error?
logger.err {projectId, error}, "error getting members for project"
return callback(error)
{owner, members} = ProjectEditorHandler.buildOwnerAndMembersViews(rawMembers)
callback(null, members)

View File

@@ -0,0 +1,123 @@
ProjectGetter = require "../Project/ProjectGetter"
LimitationsManager = require "../Subscription/LimitationsManager"
UserGetter = require "../User/UserGetter"
CollaboratorsHandler = require('./CollaboratorsHandler')
CollaboratorsInviteHandler = require('./CollaboratorsInviteHandler')
logger = require('logger-sharelatex')
EmailHelper = require "../Helpers/EmailHelper"
EditorRealTimeController = require("../Editor/EditorRealTimeController")
NotificationsBuilder = require("../Notifications/NotificationsBuilder")
module.exports = CollaboratorsInviteController =
getAllInvites: (req, res, next) ->
projectId = req.params.Project_id
logger.log {projectId}, "getting all active invites for project"
CollaboratorsInviteHandler.getAllInvites projectId, (err, invites) ->
if err?
logger.err {projectId}, "error getting invites for project"
return next(err)
res.json({invites: invites})
inviteToProject: (req, res, next) ->
projectId = req.params.Project_id
email = req.body.email
sendingUser = req.session.user
sendingUserId = sendingUser._id
logger.log {projectId, email, sendingUserId}, "inviting to project"
LimitationsManager.canAddXCollaborators projectId, 1, (error, allowed) =>
return next(error) if error?
if !allowed
logger.log {projectId, email, sendingUserId}, "not allowed to invite more users to project"
return res.json {invite: null}
{email, privileges} = req.body
email = EmailHelper.parseEmail(email)
if !email? or email == ""
logger.log {projectId, email, sendingUserId}, "invalid email address"
return res.sendStatus(400)
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})
return res.json {invite: invite}
revokeInvite: (req, res, next) ->
projectId = req.params.Project_id
inviteId = req.params.invite_id
logger.log {projectId, inviteId}, "revoking invite"
CollaboratorsInviteHandler.revokeInvite projectId, inviteId, (err) ->
if err?
logger.err {projectId, inviteId}, "error revoking invite"
return next(err)
EditorRealTimeController.emitToRoom projectId, 'project:membership:changed', {invites: true}
res.sendStatus(201)
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, sendingUser, inviteId, (err) ->
if err?
logger.err {projectId, inviteId}, "error resending invite"
return next(err)
res.sendStatus(201)
viewInvite: (req, res, next) ->
projectId = req.params.Project_id
token = req.params.token
currentUser = req.session.user
_renderInvalidPage = () ->
logger.log {projectId, token}, "invite not valid, rendering not-valid page"
res.render "project/invite/not-valid", {title: "Invalid Invite"}
# check if the user is already a member of the project
CollaboratorsHandler.isUserMemberOfProject currentUser._id, projectId, (err, isMember, _privilegeLevel) ->
if err?
logger.err {err, projectId}, "error checking if user is member of project"
return next(err)
if isMember
logger.log {projectId, userId: currentUser._id}, "user is already a member of this project, redirecting"
return res.redirect "/project/#{projectId}"
# get the invite
CollaboratorsInviteHandler.getInviteByToken projectId, token, (err, invite) ->
if err?
logger.err {projectId, token}, "error getting invite by token"
return next(err)
# check if invite is gone, or otherwise non-existent
if !invite?
logger.log {projectId, token}, "no invite found for this token"
return _renderInvalidPage()
# check the user who sent the invite exists
UserGetter.getUser {_id: invite.sendingUserId}, {email: 1, first_name: 1, last_name: 1}, (err, owner) ->
if err?
logger.err {err, projectId}, "error getting project owner"
return next(err)
if !owner?
logger.log {projectId}, "no project owner found"
return _renderInvalidPage()
# fetch the project name
ProjectGetter.getProject projectId, {}, (err, project) ->
if err?
logger.err {err, projectId}, "error getting project"
return next(err)
if !project?
logger.log {projectId}, "no project found"
return _renderInvalidPage()
# finally render the invite
res.render "project/invite/show", {invite, project, owner, title: "Project Invite"}
acceptInvite: (req, res, next) ->
projectId = req.params.Project_id
inviteId = req.params.invite_id
{token} = req.body
currentUser = req.session.user
logger.log {projectId, inviteId, userId: currentUser._id}, "accepting invite"
CollaboratorsInviteHandler.acceptInvite projectId, inviteId, token, currentUser, (err) ->
if err?
logger.err {projectId, inviteId}, "error accepting invite by token"
return next(err)
EditorRealTimeController.emitToRoom projectId, 'project:membership:changed', {invites: true, members: true}
res.redirect "/project/#{projectId}"

View File

@@ -0,0 +1,142 @@
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 =
getAllInvites: (projectId, callback=(err, invites)->) ->
logger.log {projectId}, "fetching invites for project"
ProjectInvite.find {projectId: projectId}, (err, invites) ->
if err?
logger.err {err, projectId}, "error getting invites from mongo"
return callback(err)
logger.log {projectId, count: invites.length}, "found invites for project"
callback(null, invites)
getInviteCount: (projectId, callback=(err, count)->) ->
logger.log {projectId}, "counting invites for project"
ProjectInvite.count {projectId: projectId}, (err, count) ->
if err?
logger.err {err, projectId}, "error getting invites from mongo"
return callback(err)
callback(null, count)
_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: sendingUser._id, email}, "error generating random token"
return callback(err)
token = buffer.toString('hex')
invite = new ProjectInvite {
email: email
token: token
sendingUserId: sendingUser._id
projectId: projectId
privileges: privileges
}
invite.save (err, invite) ->
if err?
logger.err {err, projectId, sendingUserId: sendingUser._id, email}, "error saving token"
return callback(err)
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"
ProjectInvite.remove {projectId: projectId, _id: inviteId}, (err) ->
if err?
logger.err {err, projectId, inviteId}, "error removing invite"
return callback(err)
CollaboratorsInviteHandler._tryCancelInviteNotification(inviteId, ()->)
callback(null)
resendInvite: (projectId, sendingUser, inviteId, callback=(err)->) ->
logger.log {projectId, inviteId}, "resending invite email"
ProjectInvite.findOne {_id: inviteId, projectId: projectId}, (err, invite) ->
if err?
logger.err {err, projectId, inviteId}, "error finding invite"
return callback(err)
if !invite?
logger.err {err, projectId, inviteId}, "no invite found, nothing to resend"
return 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"
ProjectInvite.findOne {projectId: projectId, token: tokenString}, (err, invite) ->
if err?
logger.err {err, projectId}, "error fetching invite"
return callback(err)
if !invite?
logger.err {err, projectId, token: tokenString}, "no invite found"
return callback(null, null)
callback(null, invite)
acceptInvite: (projectId, inviteId, tokenString, user, callback=(err)->) ->
logger.log {projectId, inviteId, userId: user._id}, "accepting invite"
CollaboratorsInviteHandler.getInviteByToken projectId, tokenString, (err, invite) ->
if err?
logger.err {err, projectId, inviteId}, "error finding invite"
return callback(err)
if !invite
err = new Errors.NotFoundError("no matching invite found")
logger.log {err, projectId, inviteId, tokenString}, "no matching invite found"
return callback(err)
inviteId = invite._id
CollaboratorsHandler.addUserIdToProject projectId, invite.sendingUserId, user._id, invite.privileges, (err) ->
if err?
logger.err {err, projectId, inviteId, userId: user._id}, "error adding user to project"
return callback(err)
# Remove invite
logger.log {projectId, inviteId}, "removing invite"
ProjectInvite.remove {_id: inviteId}, (err) ->
if err?
logger.err {err, projectId, inviteId}, "error removing invite"
return callback(err)
CollaboratorsInviteHandler._tryCancelInviteNotification inviteId, ()->
callback()

View File

@@ -1,6 +1,8 @@
CollaboratorsController = require('./CollaboratorsController')
AuthenticationController = require('../Authentication/AuthenticationController')
AuthorizationMiddlewear = require('../Authorization/AuthorizationMiddlewear')
CollaboratorsInviteController = require('./CollaboratorsInviteController')
RateLimiterMiddlewear = require('../Security/RateLimiterMiddlewear')
module.exports =
apply: (webRouter, apiRouter) ->
@@ -8,3 +10,63 @@ module.exports =
webRouter.post '/project/:Project_id/users', AuthorizationMiddlewear.ensureUserCanAdminProject, CollaboratorsController.addUserToProject
webRouter.delete '/project/:Project_id/users/:user_id', AuthorizationMiddlewear.ensureUserCanAdminProject, CollaboratorsController.removeUserFromProject
webRouter.get(
'/project/:Project_id/members',
AuthenticationController.requireLogin(),
AuthorizationMiddlewear.ensureUserCanAdminProject,
CollaboratorsController.getAllMembers
)
# invites
webRouter.post(
'/project/:Project_id/invite',
RateLimiterMiddlewear.rateLimit({
endpointName: "invite-to-project"
params: ["Project_id"]
maxRequests: 200
timeInterval: 60 * 10
}),
AuthenticationController.requireLogin(),
AuthorizationMiddlewear.ensureUserCanAdminProject,
CollaboratorsInviteController.inviteToProject
)
webRouter.get(
'/project/:Project_id/invites',
AuthenticationController.requireLogin(),
AuthorizationMiddlewear.ensureUserCanAdminProject,
CollaboratorsInviteController.getAllInvites
)
webRouter.delete(
'/project/:Project_id/invite/:invite_id',
AuthenticationController.requireLogin(),
AuthorizationMiddlewear.ensureUserCanAdminProject,
CollaboratorsInviteController.revokeInvite
)
webRouter.post(
'/project/:Project_id/invite/:invite_id/resend',
RateLimiterMiddlewear.rateLimit({
endpointName: "resend-invite"
params: ["Project_id"]
maxRequests: 200
timeInterval: 60 * 10
}),
AuthenticationController.requireLogin(),
AuthorizationMiddlewear.ensureUserCanAdminProject,
CollaboratorsInviteController.resendInvite
)
webRouter.get(
'/project/:Project_id/invite/token/:token',
AuthenticationController.requireLogin(),
CollaboratorsInviteController.viewInvite
)
webRouter.post(
'/project/:Project_id/invite/:invite_id/accept',
AuthenticationController.requireLogin(),
CollaboratorsInviteController.acceptInvite
)

View File

@@ -9,6 +9,7 @@ AuthorizationManager = require("../Authorization/AuthorizationManager")
ProjectEditorHandler = require('../Project/ProjectEditorHandler')
Metrics = require('../../infrastructure/Metrics')
CollaboratorsHandler = require("../Collaborators/CollaboratorsHandler")
CollaboratorsInviteHandler = require("../Collaborators/CollaboratorsInviteHandler")
PrivilegeLevels = require "../Authorization/PrivilegeLevels"
module.exports = EditorHttpController =
@@ -30,6 +31,7 @@ module.exports = EditorHttpController =
ProjectDeleter.unmarkAsDeletedByExternalSource project_id
_buildJoinProjectView: (project_id, user_id, callback = (error, project, privilegeLevel) ->) ->
logger.log {project_id, user_id}, "building the joinProject view"
ProjectGetter.getProjectWithoutDocLines project_id, (error, project) ->
return callback(error) if error?
return callback(new Error("not found")) if !project?
@@ -40,10 +42,13 @@ module.exports = EditorHttpController =
AuthorizationManager.getPrivilegeLevelForProject user_id, project_id, (error, privilegeLevel) ->
return callback(error) if error?
if !privilegeLevel? or privilegeLevel == PrivilegeLevels.NONE
callback null, null, false
else
logger.log {project_id, user_id, privilegeLevel}, "not an acceptable privilege level, returning null"
return callback null, null, false
CollaboratorsInviteHandler.getAllInvites project_id, (error, invites) ->
return callback(error) if error?
logger.log {project_id, user_id, memberCount: members.length, inviteCount: invites.length, privilegeLevel}, "returning project model view"
callback(null,
ProjectEditorHandler.buildProjectModelView(project, members),
ProjectEditorHandler.buildProjectModelView(project, members, invites),
privilegeLevel
)
@@ -135,5 +140,3 @@ module.exports = EditorHttpController =
EditorController.deleteEntity project_id, entity_id, entity_type, "editor", (error) ->
return next(error) if error?
res.sendStatus 204

View File

@@ -7,7 +7,7 @@ settings = require("settings-sharelatex")
templates = {}
templates.registered =
templates.registered =
subject: _.template "Activate your #{settings.appName} Account"
layout: PersonalEmailLayout
type: "notification"
@@ -19,7 +19,7 @@ templates.registered =
<p>If you have any questions or problems, please contact <a href="mailto:#{settings.adminEmail}">#{settings.adminEmail}</a>.</p>
"""
templates.canceledSubscription =
templates.canceledSubscription =
subject: _.template "ShareLaTeX thoughts"
layout: PersonalEmailLayout
type:"lifecycle"
@@ -36,7 +36,7 @@ ShareLaTeX Co-founder
</p>
'''
templates.passwordResetRequested =
templates.passwordResetRequested =
subject: _.template "Password Reset - #{settings.appName}"
layout: NotificationEmailLayout
type:"notification"
@@ -66,7 +66,7 @@ If you didn't request a password reset, let us know.
<p> <a href="<%= siteUrl %>">#{settings.appName}</a></p>
"""
templates.projectSharedWithYou =
templates.projectSharedWithYou =
subject: _.template "<%= owner.email %> wants to share <%= project.name %> with you"
layout: NotificationEmailLayout
type:"notification"
@@ -87,8 +87,24 @@ templates.projectSharedWithYou =
<p> <a href="<%= siteUrl %>">#{settings.appName}</a></p>
"""
templates.projectInvite =
subject: _.template "<%= project.name %> - shared by <%= owner.email %>"
layout: NotificationEmailLayout
type:"notification"
compiledTemplate: _.template """
<p>Hi, <%= owner.email %> wants to share <a href="<%= project.url %>">'<%= project.name %>'</a> with you</p>
<center>
<a style="text-decoration: none; width: 200px; background-color: #a93629; border: 1px solid #e24b3b; border-radius: 3px; padding: 15px; margin: 24px; display: block;" href="<%= inviteUrl %>" style="text-decoration:none" target="_blank">
<span style= "font-size:16px;font-family:Helvetica,Arial;font-weight:400;color:#fff;white-space:nowrap;display:block; text-align:center">
View Project
</span>
</a>
</center>
<p> Thank you</p>
<p> <a href="<%= siteUrl %>">#{settings.appName}</a></p>
"""
templates.completeJoinGroupAccount =
templates.completeJoinGroupAccount =
subject: _.template "Verify Email to join <%= group_name %> group"
layout: NotificationEmailLayout
type:"notification"
@@ -123,4 +139,3 @@ module.exports =
html: template.layout(opts)
type:template.type
}

View File

@@ -0,0 +1,11 @@
mimelib = require("mimelib")
module.exports = EmailHelper =
parseEmail: (email) ->
email = mimelib.parseAddresses(email or "")[0]?.address?.toLowerCase()
if !email? or email == ""
return null
else
return email

View File

@@ -1,16 +1,31 @@
logger = require("logger-sharelatex")
NotificationsHandler = require("./NotificationsHandler")
module.exports =
module.exports =
# Note: notification keys should be url-safe
groupPlan: (user, licence)->
key : "join-sub-#{licence.subscription_id}"
create: (callback = ->)->
messageOpts =
messageOpts =
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, callback
logger.log user_id:user._id, key:key, "creating notification key for user"
NotificationsHandler.createNotification user._id, @key, "notification_group_invite", messageOpts, null, false, callback
read: (callback = ->)->
NotificationsHandler.markAsReadWithKey user._id, @key, callback
projectInvite: (invite, project, sendingUser, user) ->
key: "project-invite-#{invite._id}"
create: (callback=()->) ->
messageOpts =
userName: sendingUser.first_name
projectName: project.name
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, true, callback
read: (callback=()->) ->
NotificationsHandler.markAsReadByKeyOnly @key, callback

View File

@@ -10,10 +10,10 @@ makeRequest = (opts, callback)->
else
request(opts, callback)
module.exports =
module.exports =
getUserNotifications: (user_id, callback)->
opts =
opts =
uri: "#{settings.apis.notifications?.url}/user/#{user_id}"
json: true
timeout: oneSecond
@@ -29,21 +29,26 @@ module.exports =
unreadNotifications = []
callback(null, unreadNotifications)
createNotification: (user_id, key, templateKey, messageOpts, callback)->
opts =
uri: "#{settings.apis.notifications?.url}/user/#{user_id}"
timeout: oneSecond
method:"POST"
json: {
createNotification: (user_id, key, templateKey, messageOpts, expiryDateTime, forceCreate, callback)->
payload = {
key:key
messageOpts:messageOpts
templateKey:templateKey
}
}
if expiryDateTime?
payload.expires = expiryDateTime
if forceCreate
payload.forceCreate = true
opts =
uri: "#{settings.apis.notifications?.url}/user/#{user_id}"
timeout: oneSecond
method:"POST"
json: payload
logger.log opts:opts, "creating notification for user"
makeRequest opts, callback
markAsReadWithKey: (user_id, key, callback)->
opts =
opts =
uri: "#{settings.apis.notifications?.url}/user/#{user_id}"
method: "DELETE"
timeout: oneSecond
@@ -52,7 +57,7 @@ module.exports =
}
logger.log user_id:user_id, key:key, "sending mark notification as read with key to notifications api"
makeRequest opts, callback
markAsRead: (user_id, notification_id, callback)->
opts =
@@ -61,3 +66,13 @@ module.exports =
timeout:oneSecond
logger.log user_id:user_id, notification_id:notification_id, "sending mark notification as read to notifications api"
makeRequest opts, callback
# removes notification by key, without regard for user_id,
# should not be exposed to user via ui/router
markAsReadByKeyOnly: (key, callback)->
opts =
uri: "#{settings.apis.notifications?.url}/key/#{key}"
method: "DELETE"
timeout: oneSecond
logger.log {key:key}, "sending mark notification as read with key-only to notifications api"
makeRequest opts, callback

View File

@@ -1,7 +1,7 @@
_ = require("underscore")
module.exports = ProjectEditorHandler =
buildProjectModelView: (project, members) ->
buildProjectModelView: (project, members, invites) ->
result =
_id : project._id
name : project.name
@@ -15,17 +15,16 @@ module.exports = ProjectEditorHandler =
deletedByExternalDataSource : project.deletedByExternalDataSource || false
deletedDocs: project.deletedDocs
members: []
owner = null
for member in members
if member.privilegeLevel == "owner"
owner = member.user
else
result.members.push @buildUserModelView member.user, member.privilegeLevel
if owner?
result.owner = @buildUserModelView owner, "owner"
invites: invites
result.features = _.defaults(owner?.features or {}, {
if !result.invites?
result.invites = []
{owner, ownerFeatures, members} = @buildOwnerAndMembersViews(members)
result.owner = owner
result.members = members
result.features = _.defaults(ownerFeatures or {}, {
collaborators: -1 # Infinite
versioning: false
dropbox:false
@@ -37,6 +36,18 @@ module.exports = ProjectEditorHandler =
return result
buildOwnerAndMembersViews: (members) ->
owner = null
ownerFeatures = null
filteredMembers = []
for member in members
if member.privilegeLevel == "owner"
ownerFeatures = member.user.features
owner = @buildUserModelView member.user, "owner"
else
filteredMembers.push @buildUserModelView member.user, member.privilegeLevel
{owner: owner, ownerFeatures: ownerFeatures, members: filteredMembers}
buildUserModelView: (user, privileges) ->
_id : user._id
first_name : user.first_name

View File

@@ -258,6 +258,7 @@ module.exports = ProjectEntityHandler =
if !foundFolder?
logger.log path:path, project_id:project._id, folderName:folderName, "making folder from mkdirp"
@addFolder project_id, parentFolder_id, folderName, (err, newFolder, parentFolder_id)->
return callback(err) if err?
newFolder.parentFolder_id = parentFolder_id
previousFolders.push newFolder
callback null, previousFolders
@@ -268,6 +269,7 @@ module.exports = ProjectEntityHandler =
async.reduce folders, [], procesFolder, (err, folders)->
return callback(err) if err?
lastFolder = folders[folders.length-1]
folders = _.select folders, (folder)->
!folder.filterOut

View File

@@ -4,6 +4,7 @@ User = require("../../models/User").User
SubscriptionLocator = require("./SubscriptionLocator")
Settings = require("settings-sharelatex")
CollaboratorsHandler = require("../Collaborators/CollaboratorsHandler")
CollaboratorsInvitesHandler = require("../Collaborators/CollaboratorsInviteHandler")
module.exports =
@@ -20,10 +21,12 @@ module.exports =
return callback(error) if error?
CollaboratorsHandler.getCollaboratorCount project_id, (error, current_number) =>
return callback(error) if error?
if current_number + x_collaborators <= allowed_number or allowed_number < 0
callback null, true
else
callback null, false
CollaboratorsInvitesHandler.getInviteCount project_id, (error, invite_count) =>
return callback(error) if error?
if current_number + invite_count + x_collaborators <= allowed_number or allowed_number < 0
callback null, true
else
callback null, false
userHasSubscriptionOrIsGroupMember: (user, callback = (err, hasSubscriptionOrIsMember)->) ->
@userHasSubscription user, (err, hasSubscription, subscription)=>
@@ -41,7 +44,7 @@ module.exports =
hasValidSubscription = subscription? and (subscription.recurlySubscription_id? or subscription?.customAccount == true)
logger.log user:user, hasValidSubscription:hasValidSubscription, subscription:subscription, "checking if user has subscription"
callback err, hasValidSubscription, subscription
userIsMemberOfGroupSubscription: (user, callback = (error, isMember, subscriptions) ->) ->
logger.log user_id: user._id, "checking is user is member of subscription groups"
SubscriptionLocator.getMemberSubscriptions user._id, (err, subscriptions = []) ->
@@ -65,4 +68,3 @@ getOwnerOfProject = (project_id, callback)->
return callback(error) if error?
User.findById project.owner_ref, (error, owner) ->
callback(error, owner)

View File

@@ -29,13 +29,15 @@ module.exports = RecurlyWrapper =
RecurlyWrapper.apiRequest({
url: "accounts/#{user._id}"
method: "GET"
expect404: true
}, (error, response, responseBody) ->
if error
if response.statusCode == 404 # actually not an error in this case, just no existing account
cache.userExists = false
return next(null, cache)
logger.error {error, user_id: user._id, recurly_token_id}, "error response from recurly while checking account"
return next(error)
if response.statusCode == 404 # actually not an error in this case, just no existing account
logger.log {user_id: user._id, recurly_token_id}, "user does not currently exist in recurly, proceed"
cache.userExists = false
return next(null, cache)
logger.log {user_id: user._id, recurly_token_id}, "user appears to exist in recurly"
RecurlyWrapper._parseAccountXml responseBody, (err, account) ->
if err
@@ -236,10 +238,14 @@ module.exports = RecurlyWrapper =
"Authorization" : "Basic " + new Buffer(Settings.apis.recurly.apiKey).toString("base64")
"Accept" : "application/xml"
"Content-Type" : "application/xml; charset=utf-8"
expect404 = options.expect404
delete options.expect404
request options, (error, response, body) ->
unless error? or response.statusCode == 200 or response.statusCode == 201 or response.statusCode == 204
unless error? or response.statusCode == 200 or response.statusCode == 201 or response.statusCode == 204 or (response.statusCode == 404 and expect404)
logger.err err:error, body:body, options:options, statusCode:response?.statusCode, "error returned from recurly"
error = "Recurly API returned with status code: #{response.statusCode}"
if response.statusCode == 404 and expect404
logger.log {url: options.url, method: options.method}, "got 404 response from recurly, expected as valid response"
callback(error, response, body)
sign : (parameters, callback) ->

View File

@@ -22,6 +22,7 @@ module.exports = SubscriptionController =
viewName = "#{viewName}_#{req.query.v}"
logger.log viewName:viewName, "showing plans page"
GeoIpLookup.getCurrencyCode req.query?.ip || req.ip, (err, recomendedCurrency)->
return next(err) if err?
res.render viewName,
title: "plans_and_pricing"
plans: plans
@@ -71,12 +72,13 @@ module.exports = SubscriptionController =
AuthenticationController.getLoggedInUser req, (error, user) =>
return next(error) if error?
LimitationsManager.userHasSubscriptionOrIsGroupMember user, (err, hasSubOrIsGroupMember, subscription)->
return next(err) if err?
groupLicenceInviteUrl = SubscriptionDomainHandler.getDomainLicencePage(user)
if subscription?.customAccount
logger.log user: user, "redirecting to custom account page"
res.redirect "/user/subscription/custom_account"
else if groupLicenceInviteUrl? and !hasSubOrIsGroupMember
logger.log user:user, "redirecting to group subscription invite page"
logger.log user:user, "redirecting to group subscription invite page"
res.redirect groupLicenceInviteUrl
else if !hasSubOrIsGroupMember
logger.log user: user, "redirecting to plans"
@@ -99,7 +101,9 @@ module.exports = SubscriptionController =
userCustomSubscriptionPage: (req, res, next)->
AuthenticationController.getLoggedInUser req, (error, user) ->
return next(error) if error?
LimitationsManager.userHasSubscriptionOrIsGroupMember user, (err, hasSubOrIsGroupMember, subscription)->
return next(err) if err?
if !subscription?
err = new Error("subscription null for custom account, user:#{user?._id}")
logger.warn err:err, "subscription is null for custom accounts page"
@@ -113,6 +117,7 @@ module.exports = SubscriptionController =
AuthenticationController.getLoggedInUser req, (error, user) ->
return next(error) if error?
LimitationsManager.userHasSubscription user, (err, hasSubscription)->
return next(err) if err?
if !hasSubscription
res.redirect "/user/subscription"
else
@@ -142,54 +147,67 @@ module.exports = SubscriptionController =
return res.sendStatus 500
res.sendStatus 201
successful_subscription: (req, res)->
successful_subscription: (req, res, next)->
AuthenticationController.getLoggedInUser req, (error, user) =>
return next(error) if error?
SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel user, (error, subscription) ->
return next(error) if error?
res.render "subscriptions/successful_subscription",
title: "thank_you"
subscription:subscription
cancelSubscription: (req, res, next) ->
AuthenticationController.getLoggedInUser req, (error, user) ->
logger.log user_id:user._id, "canceling subscription"
return next(error) if error?
logger.log user_id:user._id, "canceling subscription"
SubscriptionHandler.cancelSubscription user, (err)->
if err?
logger.err err:err, user_id:user._id, "something went wrong canceling subscription"
return next(err)
res.redirect "/user/subscription"
updateSubscription: (req, res)->
updateSubscription: (req, res, next)->
_origin = req?.query?.origin || null
AuthenticationController.getLoggedInUser req, (error, user) ->
return next(error) if error?
planCode = req.body.plan_code
if !planCode?
err = new Error('plan_code is not defined')
logger.err {user_id: user._id, err, planCode, origin: _origin, body: req.body}, "[Subscription] error in updateSubscription form"
return next(err)
logger.log planCode: planCode, user_id:user._id, "updating subscription"
SubscriptionHandler.updateSubscription user, planCode, null, (err)->
if err?
logger.err err:err, user_id:user._id, "something went wrong updating subscription"
return next(err)
res.redirect "/user/subscription"
reactivateSubscription: (req, res)->
reactivateSubscription: (req, res, next)->
AuthenticationController.getLoggedInUser req, (error, user) ->
logger.log user_id:user._id, "reactivating subscription"
return next(error) if error?
logger.log user_id:user._id, "reactivating subscription"
SubscriptionHandler.reactivateSubscription user, (err)->
if err?
logger.err err:err, user_id:user._id, "something went wrong reactivating subscription"
return next(err)
res.redirect "/user/subscription"
recurlyCallback: (req, res)->
recurlyCallback: (req, res, next)->
logger.log data: req.body, "received recurly callback"
# we only care if a subscription has exipired
if req.body? and req.body["expired_subscription_notification"]?
recurlySubscription = req.body["expired_subscription_notification"].subscription
SubscriptionHandler.recurlyCallback recurlySubscription, ->
SubscriptionHandler.recurlyCallback recurlySubscription, (err)->
return next(err) if err?
res.sendStatus 200
else
res.sendStatus 200
renderUpgradeToAnnualPlanPage: (req, res)->
renderUpgradeToAnnualPlanPage: (req, res, next)->
AuthenticationController.getLoggedInUser req, (error, user) ->
return next(error) if error?
LimitationsManager.userHasSubscription user, (err, hasSubscription, subscription)->
return next(err) if err?
planCode = subscription?.planCode.toLowerCase()
if planCode?.indexOf("annual") != -1
planName = "annual"
@@ -204,8 +222,9 @@ module.exports = SubscriptionController =
title: "Upgrade to annual"
planName: planName
processUpgradeToAnnualPlan: (req, res)->
processUpgradeToAnnualPlan: (req, res, next)->
AuthenticationController.getLoggedInUser req, (error, user) ->
return next(error) if error?
{planName} = req.body
coupon_code = Settings.coupon_codes.upgradeToAnnualPromo[planName]
annualPlanName = "#{planName}-annual"
@@ -213,13 +232,14 @@ module.exports = SubscriptionController =
SubscriptionHandler.updateSubscription user, annualPlanName, coupon_code, (err)->
if err?
logger.err err:err, user_id:user._id, "error updating subscription"
res.sendStatus 500
else
res.sendStatus 200
return next(err)
res.sendStatus 200
extendTrial: (req, res)->
extendTrial: (req, res, next)->
AuthenticationController.getLoggedInUser req, (error, user) ->
return next(error) if error?
LimitationsManager.userHasSubscription user, (err, hasSubscription, subscription)->
return next(err) if err?
SubscriptionHandler.extendTrial subscription, 14, (err)->
if err?
res.send 500

View File

@@ -1,23 +1,42 @@
mongoose = require 'mongoose'
Settings = require 'settings-sharelatex'
Schema = mongoose.Schema
ObjectId = Schema.ObjectId
ProjectInviteSchema = new Schema
project_id: ObjectId
from_user_id: ObjectId
privilegeLevel: String
# For existing users
to_user_id: ObjectId
# For non-existant users
hashed_token: String
email: String
EXPIRY_IN_SECONDS = 60 * 60 * 24 * 30
ExpiryDate = () ->
timestamp = new Date()
timestamp.setSeconds(timestamp.getSeconds() + EXPIRY_IN_SECONDS)
return timestamp
ProjectInviteSchema = new Schema(
{
email: String
token: String
sendingUserId: ObjectId
projectId: ObjectId
privileges: String
createdAt: {type: Date, default: Date.now}
expires: {type: Date, default: ExpiryDate, index: {expireAfterSeconds: 10}}
},
{
collection: 'projectInvites'
}
)
conn = mongoose.createConnection(Settings.mongo.url, server: poolSize: Settings.mongo.poolSize || 10)
ProjectInvite = conn.model('ProjectInvite', ProjectInviteSchema)
mongoose.model 'ProjectInvite', ProjectInviteSchema
exports.ProjectInvite = ProjectInvite
exports.ProjectInviteSchema = ProjectInviteSchema
exports.ProjectInviteSchema = ProjectInviteSchema
exports.EXPIRY_IN_SECONDS = EXPIRY_IN_SECONDS

View File

@@ -41,11 +41,12 @@ aside.chat(
}"
)
.arrow(ng-style="{'border-color': 'hsl({{ hue(message.user) }}, 70%, 70%)'}")
p(
mathjax,
ng-repeat="content in message.contents track by $index"
)
span(ng-bind-html="content | linky:'_blank' | wrapLongWords")
.message-content
p(
mathjax,
ng-repeat="content in message.contents track by $index"
)
span(ng-bind-html="content | linky:'_blank'")
.new-message
textarea(

View File

@@ -27,12 +27,12 @@ script(type='text/ng-template', id='shareProjectModalTemplate')
) #{translate("make_private")}
.row.project-member
.col-xs-8 {{ project.owner.email }}
.text-right(
.text-left(
ng-class="{'col-xs-3': project.members.length > 0, 'col-xs-4': project.members.length == 0}"
) #{translate("owner")}
.row.project-member(ng-repeat="member in project.members")
.col-xs-8 {{ member.email }}
.col-xs-3.text-right
.col-xs-3.text-left
span(ng-show="member.privileges == 'readAndWrite'") #{translate("can_edit")}
span(ng-show="member.privileges == 'readOnly'") #{translate("read_only")}
.col-xs-1
@@ -43,6 +43,23 @@ script(type='text/ng-template', id='shareProjectModalTemplate')
ng-click="removeMember(member)"
)
i.fa.fa-times
.row.project-invite(ng-repeat="invite in project.invites")
.col-xs-8 {{ invite.email }}&nbsp;
div.small
| #{translate("invite_not_accepted")}.&nbsp;
a(href="#", ng-click="resendInvite(invite, $event)") #{translate("resend")}
.col-xs-3.text-left
// todo: get invite privileges
span(ng-show="invite.privileges == 'readAndWrite'") #{translate("can_edit")}
span(ng-show="invite.privileges == 'readOnly'") #{translate("read_only")}
.col-xs-1
a(
href
tooltip="#{translate('revoke_invite')}"
tooltip-placement="bottom"
ng-click="revokeInvite(invite)"
)
i.fa.fa-times
.row.invite-controls
form(ng-show="canAddCollaborators")
.small #{translate("share_with_your_collabs")}
@@ -78,6 +95,7 @@ script(type='text/ng-template', id='shareProjectModalTemplate')
button.btn.btn-info(
type="submit"
ng-mousedown="addMembers()"
ng-keyup="$event.keyCode == 13 ? addMembers() : null"
) #{translate("share")}
div(ng-hide="canAddCollaborators")
p.text-center #{translate("need_to_upgrade_for_more_collabs")}. Also:
@@ -123,7 +141,7 @@ script(type='text/ng-template', id='shareProjectModalTemplate')
.modal-footer-left
i.fa.fa-refresh.fa-spin(ng-show="state.inflight")
span.text-danger.error(ng-show="state.error") #{translate("generic_something_went_wrong")}
button.btn.btn-primary(
button.btn.btn-default(
ng-click="done()"
) #{translate("close")}

View File

@@ -0,0 +1,18 @@
extends ../../layout
block content
.content.content-alt
.container
.row
.col-md-8.col-md-offset-2
.card.project-invite-invalid
.page-header.text-centered
h1 #{translate("invite_not_valid")}
.row.text-center
.col-md-12
p
| #{translate("invite_not_valid_description")}.
.row.text-center.actions
.col-md-12
a.btn.btn-info(href="/project") #{translate("back_to_your_projects")}

View File

@@ -0,0 +1,31 @@
extends ../../layout
block content
.content.content-alt
.container
.row
.col-md-8.col-md-offset-2
.card.project-invite-accept
.page-header.text-centered
h1 #{translate("user_wants_you_to_see_project", {username:owner.first_name, projectname:""})}
em
span.project-name #{project.name}
.row.text-center
.col-md-12
p
| #{translate("accepting_invite_as")}&nbsp;
em #{user.email}
.row
.col-md-12
form.form(
name="acceptForm",
method="POST",
action="/project/#{invite.projectId}/invite/#{invite._id}/accept"
)
input(name='_csrf', type='hidden', value=csrfToken)
input(name='token', type='hidden', value="#{invite.token}")
.form-group.text-center
button.btn.btn-lg.btn-primary(type="submit")
| #{translate("join_project")}
.form-group.text-center

View File

@@ -9,7 +9,9 @@ span(ng-controller="NotificationsController").userNotifications
.row(ng-hide="unreadNotification.hide")
.col-xs-12
.alert.alert-info
span(ng-bind-html="unreadNotification.html")
button(ng-click="dismiss(unreadNotification)").close.pull-right
span(aria-hidden="true") &times;
span.sr-only #{translate("close")}
div.notification_inner
span(ng-bind-html="unreadNotification.html").notification_body
span().notification_close
button(ng-click="dismiss(unreadNotification)").close.pull-right
span(aria-hidden="true") &times;
span.sr-only #{translate("close")}

View File

@@ -19,7 +19,7 @@ block content
Recurly.config(!{recurlyConfig})
Recurly.buildBillingInfoUpdateForm({
target : "#billingDetailsForm",
successURL : "#{successURL}?_csrf=#{csrfToken}",
successURL : "#{successURL}?_csrf=#{csrfToken}&origin=editBillingDetails",
signature : "!{signature}",
accountCode : "#{user.id}"
});

View File

@@ -6,9 +6,13 @@ block content
.row
.registration_message
if sharedProjectData.user_first_name !== undefined
h1 #{translate("user_wants_you_to_see_project", {username:sharedProjectData.user_first_name, projectname:sharedProjectData.project_name})}
div #{translate("join_sl_to_view_project")}.
a(href="/login") #{translate("login_here")}
h1 #{translate("user_wants_you_to_see_project", {username:sharedProjectData.user_first_name, projectname:""})}
em #{sharedProjectData.project_name}
div
| #{translate("join_sl_to_view_project")}.
div
| #{translate("if_you_are_registered")},
a(href="/login?redir=#{getReqQueryParam('redir')}") #{translate("login_here")}
else if newTemplateData.templateName !== undefined
h1 #{translate("register_to_edit_template", {templateName:newTemplateData.templateName})}

View File

@@ -11,6 +11,8 @@ define [
_getRule = (logMessage) ->
return rule for rule in ruleset when rule.regexToMatch.test logMessage
seenErrorTypes = {} # keep track of types of errors seen
for entry in parsedLogEntries.all
ruleDetails = _getRule entry.message
@@ -21,8 +23,20 @@ define [
entry.ruleId = 'hint_' + ruleDetails.regexToMatch.toString().replace(/\s/g, '_').slice(1, -1)
if ruleDetails.newMessage?
entry.message = entry.message.replace ruleDetails.regexToMatch, ruleDetails.newMessage
# suppress any entries that are known to cascade from previous error types
if ruleDetails.cascadesFrom?
for type in ruleDetails.cascadesFrom
entry.suppressed = true if seenErrorTypes[type]
# record the types of errors seen
if ruleDetails.types?
for type in ruleDetails.types
seenErrorTypes[type] = true
entry.humanReadableHint = ruleDetails.humanReadableHint if ruleDetails.humanReadableHint?
entry.extraInfoURL = ruleDetails.extraInfoURL if ruleDetails.extraInfoURL?
# filter out the suppressed errors (from the array entries in parsedLogEntries)
for key, errors of parsedLogEntries when typeof errors is 'object' and errors.length > 0
parsedLogEntries[key] = (err for err in errors when not err.suppressed)
return parsedLogEntries

View File

@@ -90,6 +90,7 @@ define -> [
"""
,
ruleId: "hint_mismatched_environment"
types: ['environment']
regexToMatch: /Error: `([^']{2,})' expected, found `([^']{2,})'.*/
newMessage: "Error: environment does not match \\begin{$1} ... \\end{$2}"
humanReadableHint: """
@@ -97,10 +98,85 @@ define -> [
"""
,
ruleId: "hint_mismatched_brackets"
types: ['environment']
regexToMatch: /Error: `([^a-zA-Z0-9])' expected, found `([^a-zA-Z0-9])'.*/
newMessage: "Error: brackets do not match, found '$2' instead of '$1'"
humanReadableHint: """
You have used an open bracket without a corresponding close bracket.
"""
,
regexToMatch: /LaTeX Error: Can be used only in preamble/
extraInfoURL: "https://www.sharelatex.com/learn/Errors/LaTeX_Error:_Can_be_used_only_in_preamble"
humanReadableHint: """
You have used a command in the main body of your document which should be used in the preamble. Make sure that \\documentclass[\u2026]{\u2026} and all \\usepackage{\u2026} commands are written before \\begin{document}.
"""
,
regexToMatch: /Missing \\right inserted/
extraInfoURL: "https://www.sharelatex.com/learn/Errors/Missing_%5Cright_insertede"
humanReadableHint: """
You have started an expression with a \\left command, but have not included a corresponding \\right command. Make sure that your \\left and \\right commands balance everywhere, or else try using \\Biggl and \\Biggr commands instead as shown <a target=\"_blank\" href=\"https://www.sharelatex.com/learn/Errors/Missing_%5Cright_inserted\">here</a>.
"""
,
regexToMatch: /Double superscript/
extraInfoURL: "https://www.sharelatex.com/learn/Errors/Double_superscript"
humanReadableHint: """
You have written a double superscript incorrectly as a^b^c, or else you have written a prime with a superscript. Remember to include { and } when using multiple superscripts. Try a^{b^c} instead.
"""
,
regexToMatch: /Double subscript/
extraInfoURL: "https://www.sharelatex.com/learn/Errors/Double_subscript"
humanReadableHint: """
You have written a double subscript incorrectly as a_b_c. Remember to include { and } when using multiple subscripts. Try a_{b_c} instead.
"""
,
regexToMatch: /No \\author given/
extraInfoURL: "https://www.sharelatex.com/learn/Errors/No_%5Cauthor_given"
humanReadableHint: """
You have used the \\maketitle command, but have not specified any \\author. To fix this, include an author in your preamble using the \\author{\u2026} command.
"""
,
regexToMatch: /LaTeX Error: Environment .+ undefined/
extraInfoURL: "https://www.sharelatex.com/learn/Errors%2FLaTeX%20Error%3A%20Environment%20XXX%20undefined"
humanReadableHint: """
You have created an environment (using \\begin{\u2026} and \\end{\u2026} commands) which is not recognized. Make sure you have included the required package for that environment in your preamble, and that the environment is spelled correctly.
"""
,
regexToMatch: /LaTeX Error: Something's wrong--perhaps a missing \\item/
extraInfoURL: "https://www.sharelatex.com/learn/Errors/LaTeX_Error:_Something%27s_wrong--perhaps_a_missing_%5Citem"
humanReadableHint: """
There are no entries found in a list you have created. Make sure you label list entries using the \\item command, and that you have not used a list inside a table.
"""
,
regexToMatch: /Misplaced \\noalign/
extraInfoURL: "https://www.sharelatex.com/learn/Errors/Misplaced_%5Cnoalign"
humanReadableHint: """
You have used a \\hline command in the wrong place, probably outside a table. If the \\hline command is written inside a table, try including \\\ before it.
"""
,
ruleId: "hint_mismatched_environment2"
types: ['environment']
cascadesFrom: ['environment']
regexToMatch: /Error: `\\end\{([^\}]+)\}' expected but found `\\end\{([^\}]+)\}'.*/
newMessage: "Error: environments do not match: \\begin{$1} ... \\end{$2}"
humanReadableHint: """
You have used \\begin{} without a corresponding \\end{}.
"""
,
ruleId: "hint_mismatched_environment3"
types: ['environment']
cascadesFrom: ['environment']
regexToMatch: /Warning: No matching \\end found for `\\begin\{([^\}]+)\}'.*/
newMessage: "Warning: No matching \\end found for \\begin{$1}"
humanReadableHint: """
You have used \\begin{} without a corresponding \\end{}.
"""
,
ruleId: "hint_mismatched_environment4"
types: ['environment']
cascadesFrom: ['environment']
regexToMatch: /Error: Found `\\end\{([^\}]+)\}' without corresponding \\begin.*/
newMessage: "Error: found \\end{$1} without a corresponding \\begin{$1}"
humanReadableHint: """
You have used \\begin{} without a corresponding \\end{}.
"""
]

View File

@@ -1,13 +1,30 @@
define [
"base"
], (App) ->
App.controller "ShareController", ["$scope", "$modal", "event_tracking", ($scope, $modal, event_tracking) ->
$scope.openShareProjectModal = () ->
event_tracking.sendMBOnce "ide-open-share-modal-once"
App.controller "ShareController", ["$scope", "$modal", "ide", "projectInvites", "projectMembers", "event_tracking",
($scope, $modal, ide, projectInvites, projectMembers, event_tracking) ->
$scope.openShareProjectModal = () ->
event_tracking.sendMBOnce "ide-open-share-modal-once"
$modal.open(
templateUrl: "shareProjectModalTemplate"
controller: "ShareProjectModalController"
scope: $scope
)
$modal.open(
templateUrl: "shareProjectModalTemplate"
controller: "ShareProjectModalController"
scope: $scope
)
ide.socket.on 'project:membership:changed', (data) =>
if data.members
projectMembers.getMembers()
.success (responseData) =>
if responseData.members
$scope.project.members = responseData.members
.error (responseDate) =>
console.error "Error fetching members for project"
if data.invites
projectInvites.getInvites()
.success (responseData) =>
if responseData.invites
$scope.project.invites = responseData.invites
.error (responseDate) =>
console.error "Error fetching invites for project"
]

View File

@@ -1,7 +1,7 @@
define [
"base"
], (App) ->
App.controller "ShareProjectModalController", ($scope, $modalInstance, $timeout, projectMembers, $modal, $http) ->
App.controller "ShareProjectModalController", ($scope, $modalInstance, $timeout, projectMembers, projectInvites, $modal, $http) ->
$scope.inputs = {
privileges: "readAndWrite"
contacts: []
@@ -10,6 +10,7 @@ define [
error: null
inflight: false
startedFreeTrial: false
invites: []
}
$modalInstance.opened.then () ->
@@ -22,6 +23,8 @@ define [
allowedNoOfMembers = $scope.project.features.collaborators
$scope.canAddCollaborators = noOfMembers < allowedNoOfMembers or allowedNoOfMembers == INFINITE_COLLABORATORS
window._m = projectMembers
$scope.autocompleteContacts = []
do loadAutocompleteUsers = () ->
$http.get "/user/contacts"
@@ -38,9 +41,12 @@ define [
else
# Must be a group
contact.display = contact.name
getCurrentMemberEmails = () ->
$scope.project.members.map (u) -> u.email
($scope.project.members || []).map (u) -> u.email
getCurrentInviteEmails = () ->
($scope.project.invites || []).map (u) -> u.email
$scope.filterAutocompleteUsers = ($query) ->
currentMemberEmails = getCurrentMemberEmails()
@@ -60,36 +66,47 @@ define [
$scope.inputs.contacts = []
$scope.state.error = null
$scope.state.inflight = true
if !$scope.project.invites?
$scope.project.invites = []
currentMemberEmails = getCurrentMemberEmails()
currentInviteEmails = getCurrentInviteEmails()
do addNextMember = () ->
if members.length == 0 or !$scope.canAddCollaborators
$scope.state.inflight = false
$scope.$apply()
return
member = members.shift()
if !member.type? and member.display in currentMemberEmails
# Skip this existing member
return addNextMember()
if member.type == "user"
request = projectMembers.addMember(member.email, $scope.inputs.privileges)
# NOTE: groups aren't really a thing in ShareLaTeX, partially inherited from DJ
if member.display in currentInviteEmails and inviteId = _.find(($scope.project.invites || []), (invite) -> invite.email == member.display)?._id
request = projectInvites.resendInvite(inviteId)
else if member.type == "user"
request = projectInvites.sendInvite(member.email, $scope.inputs.privileges)
else if member.type == "group"
request = projectMembers.addGroup(member.id, $scope.inputs.privileges)
else # Not an auto-complete object, so email == display
request = projectMembers.addMember(member.display, $scope.inputs.privileges)
request = projectInvites.sendInvite(member.display, $scope.inputs.privileges)
request
.success (data) ->
if data.users?
users = data.users
else if data.user?
users = [data.user]
if data.invite
invite = data.invite
$scope.project.invites.push invite
else
users = []
$scope.project.members.push users...
if data.users?
users = data.users
else if data.user?
users = [data.user]
else
users = []
$scope.project.members.push users...
setTimeout () ->
# Give $scope a chance to update $scope.canAddCollaborators
# with new collaborator information.
@@ -98,8 +115,7 @@ define [
.error () ->
$scope.state.inflight = false
$scope.state.error = true
$timeout addMembers, 50 # Give email list a chance to update
$scope.removeMember = (member) ->
@@ -116,6 +132,33 @@ define [
$scope.state.inflight = false
$scope.state.error = "Sorry, something went wrong :("
$scope.revokeInvite = (invite) ->
$scope.state.error = null
$scope.state.inflight = true
projectInvites
.revokeInvite(invite._id)
.success () ->
$scope.state.inflight = false
index = $scope.project.invites.indexOf(invite)
return if index == -1
$scope.project.invites.splice(index, 1)
.error () ->
$scope.state.inflight = false
$scope.state.error = "Sorry, something went wrong :("
$scope.resendInvite = (invite, event) ->
$scope.state.error = null
$scope.state.inflight = true
projectInvites
.resendInvite(invite._id)
.success () ->
$scope.state.inflight = false
event.target.blur()
.error () ->
$scope.state.inflight = false
$scope.state.error = "Sorry, something went wrong resending the invite :("
event.target.blur()
$scope.openMakePublicModal = () ->
$modal.open {
templateUrl: "makePublicModalTemplate"
@@ -158,4 +201,4 @@ define [
$scope.cancel = () ->
$modalInstance.dismiss()
]
]

View File

@@ -2,4 +2,5 @@ define [
"ide/share/controllers/ShareController"
"ide/share/controllers/ShareProjectModalController"
"ide/share/services/projectMembers"
], () ->
"ide/share/services/projectInvites"
], () ->

View File

@@ -0,0 +1,35 @@
define [
"base"
], (App) ->
App.factory "projectInvites", ["ide", "$http", (ide, $http) ->
return {
sendInvite: (email, privileges) ->
$http.post("/project/#{ide.project_id}/invite", {
email: email
privileges: privileges
_csrf: window.csrfToken
})
revokeInvite: (inviteId) ->
$http({
url: "/project/#{ide.project_id}/invite/#{inviteId}"
method: "DELETE"
headers:
"X-Csrf-Token": window.csrfToken
})
resendInvite: (inviteId, privileges) ->
$http.post("/project/#{ide.project_id}/invite/#{inviteId}/resend", {
_csrf: window.csrfToken
})
getInvites: () ->
$http.get("/project/#{ide.project_id}/invites", {
json: true
headers:
"X-Csrf-Token": window.csrfToken
})
}
]

View File

@@ -11,19 +11,19 @@ define [
"X-Csrf-Token": window.csrfToken
})
addMember: (email, privileges) ->
$http.post("/project/#{ide.project_id}/users", {
email: email
privileges: privileges
_csrf: window.csrfToken
})
addGroup: (group_id, privileges) ->
$http.post("/project/#{ide.project_id}/group", {
group_id: group_id
privileges: privileges
_csrf: window.csrfToken
})
getMembers: () ->
$http.get("/project/#{ide.project_id}/members", {
json: true
headers:
"X-Csrf-Token": window.csrfToken
})
}
]
]

View File

@@ -62,8 +62,7 @@ define [
$scope.inflight = true
$http.post(SUBSCRIPTION_URL, body)
$http.post("#{SUBSCRIPTION_URL}?origin=confirmChangePlan", body)
.success ->
location.reload()
.error ->
@@ -124,7 +123,7 @@ define [
plan_code: 'student'
_csrf : window.csrfToken
$scope.inflight = true
$http.post(SUBSCRIPTION_URL, body)
$http.post("#{SUBSCRIPTION_URL}?origin=downgradeToStudent", body)
.success ->
location.reload()
.error ->

View File

@@ -47,6 +47,7 @@
}
.message-wrapper {
margin-left: 50px + @line-height-computed/2;
.name {
font-size: 12px;
color: @gray-light;
@@ -54,12 +55,17 @@
min-height: 16px;
}
.message {
padding: @line-height-computed / 2;
border-left: 3px solid transparent;
position: relative;
font-size: 14px;
box-shadow: -1px 2px 3px #ddd;
border-raduis: @border-radius-base;
position: relative;
.message-content {
padding: @line-height-computed / 2;
overflow-x: auto;
}
.arrow {
right: 100%;
top: @line-height-computed / 4;

View File

@@ -6,7 +6,7 @@
font-size: 1rem;
}
.project-member, .public-access-level {
.project-member, .project-invite, .public-access-level {
padding: (@line-height-computed / 2) 0;
border-bottom: 1px solid @gray-lighter;
font-size: 14px;
@@ -19,7 +19,7 @@
padding-bottom: @line-height-computed;
}
.project-member {
.project-member, .project-invite {
&:hover {
background-color: @gray-lightest;
}

View File

@@ -0,0 +1,18 @@
.project-invite-accept {
.page-header {
.project-name {
white-space: pre;
}
}
form {
padding-top: 15px;
}
margin-bottom: 30px;
}
.project-invite-invalid {
.actions {
padding-top: 15px;
}
margin-bottom: 30px;
}

View File

@@ -32,10 +32,24 @@
ul {
margin-bottom:0px;
}
.alert {
.box-shadow(2px 4px 6px rgba(0, 0, 0, 0.25));
.notification_entry {
.alert {
.box-shadow(2px 4px 6px rgba(0, 0, 0, 0.25));
.notification_inner {
display: table-row;
.notification_body {
display: table-cell;
width: 99%;
padding-right: 15px;
vertical-align: middle;
}
.notification_close {
display: table-cell;
vertical-align: middle;
}
}
}
}
}
ul.folders-menu {

View File

@@ -77,8 +77,8 @@
@import "app/contact-us.less";
@import "app/subscription.less";
@import "app/sprites.less";
@import "app/invite.less";
@import "../js/libs/pdfListView/TextLayer.css";
@import "../js/libs/pdfListView/AnnotationsLayer.css";
@import "../js/libs/pdfListView/HighlightsLayer.css";

View File

@@ -18,6 +18,7 @@ describe "CollaboratorsController", ->
'../Subscription/LimitationsManager' : @LimitationsManager = {}
'../Project/ProjectEditorHandler' : @ProjectEditorHandler = {}
'../User/UserGetter': @UserGetter = {}
'logger-sharelatex': @logger = {err: sinon.stub(), erro: sinon.stub(), log: sinon.stub()}
@res = new MockResponse()
@req = new MockRequest()
@@ -73,10 +74,10 @@ describe "CollaboratorsController", ->
beforeEach ->
@LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, null, false)
@CollaboratorsController.addUserToProject @req, @res, @next
it "should not add the user to the project", ->
@CollaboratorsHandler.addEmailToProject.called.should.equal false
it "should not emit a userAddedToProject event", ->
@EditorRealTimeController.emitToRoom.called.should.equal false
@@ -93,17 +94,17 @@ describe "CollaboratorsController", ->
@res.status = sinon.stub().returns @res
@res.send = sinon.stub()
@CollaboratorsController.addUserToProject @req, @res, @next
it "should not add the user to the project", ->
@CollaboratorsHandler.addEmailToProject.called.should.equal false
it "should not emit a userAddedToProject event", ->
@EditorRealTimeController.emitToRoom.called.should.equal false
it "should return a 400 response", ->
@res.status.calledWith(400).should.equal true
@res.send.calledWith("invalid email address").should.equal true
describe "removeUserFromProject", ->
beforeEach ->
@req.params =
@@ -113,20 +114,23 @@ describe "CollaboratorsController", ->
@EditorRealTimeController.emitToRoom = sinon.stub()
@CollaboratorsHandler.removeUserFromProject = sinon.stub().callsArg(2)
@CollaboratorsController.removeUserFromProject @req, @res
it "should from the user from the project", ->
@CollaboratorsHandler.removeUserFromProject
.calledWith(@project_id, @user_id)
.should.equal true
it "should emit a userRemovedFromProject event to the proejct", ->
@EditorRealTimeController.emitToRoom
.calledWith(@project_id, 'userRemovedFromProject', @user_id)
.should.equal true
it "should send the back a success response", ->
@res.sendStatus.calledWith(204).should.equal true
it 'should have called emitToRoom', ->
@EditorRealTimeController.emitToRoom.calledWith(@project_id, 'project:membership:changed').should.equal true
describe "removeSelfFromProject", ->
beforeEach ->
@req.session =
@@ -141,7 +145,7 @@ describe "CollaboratorsController", ->
@CollaboratorsHandler.removeUserFromProject
.calledWith(@project_id, @user_id)
.should.equal true
it "should emit a userRemovedFromProject event to the proejct", ->
@EditorRealTimeController.emitToRoom
.calledWith(@project_id, 'userRemovedFromProject', @user_id)
@@ -150,3 +154,37 @@ describe "CollaboratorsController", ->
it "should return a success code", ->
@res.sendStatus.calledWith(204).should.equal true
describe 'getAllMembers', ->
beforeEach ->
@req.session =
user: _id: @user_id = "user-id-123"
@req.params = Project_id: @project_id
@res.json = sinon.stub()
@next = sinon.stub()
@members = [{a: 1}]
@CollaboratorsHandler.getAllMembers = sinon.stub().callsArgWith(1, null, @members)
@CollaboratorsController.getAllMembers(@req, @res, @next)
it 'should not produce an error', ->
@next.callCount.should.equal 0
it 'should produce a json response', ->
@res.json.callCount.should.equal 1
@res.json.calledWith({members: @members}).should.equal true
it 'should call CollaboratorsHandler.getAllMembers', ->
@CollaboratorsHandler.getAllMembers.callCount.should.equal 1
describe 'when CollaboratorsHandler.getAllMembers produces an error', ->
beforeEach ->
@res.json = sinon.stub()
@next = sinon.stub()
@CollaboratorsHandler.getAllMembers = sinon.stub().callsArgWith(1, new Error('woops'))
@CollaboratorsController.getAllMembers(@req, @res, @next)
it 'should produce an error', ->
@next.callCount.should.equal 1
@next.firstCall.args[0].should.be.instanceof Error
it 'should not produce a json response', ->
@res.json.callCount.should.equal 0

View File

@@ -18,13 +18,14 @@ describe "CollaboratorsHandler", ->
"../Project/ProjectEntityHandler": @ProjectEntityHandler = {}
"./CollaboratorsEmailHandler": @CollaboratorsEmailHandler = {}
"../Errors/Errors": Errors
"../Project/ProjectEditorHandler": @ProjectEditorHandler = {}
@project_id = "mock-project-id"
@user_id = "mock-user-id"
@adding_user_id = "adding-user-id"
@email = "joe@sharelatex.com"
@callback = sinon.stub()
describe "getMemberIdsWithPrivilegeLevels", ->
describe "with project", ->
beforeEach ->
@@ -46,16 +47,16 @@ describe "CollaboratorsHandler", ->
{ id: "read-write-ref-2", privilegeLevel: "readAndWrite" }
])
.should.equal true
describe "with a missing project", ->
beforeEach ->
@Project.findOne = sinon.stub().yields(null, null)
it "should return a NotFoundError", (done) ->
@CollaboratorHandler.getMemberIdsWithPrivilegeLevels @project_id, (error) ->
error.should.be.instanceof Errors.NotFoundError
done()
describe "getMemberIds", ->
beforeEach ->
@CollaboratorHandler.getMemberIdsWithPrivilegeLevels = sinon.stub()
@@ -68,7 +69,7 @@ describe "CollaboratorsHandler", ->
@callback
.calledWith(null, ["member-id-1", "member-id-2"])
.should.equal true
describe "getMembersWithPrivilegeLevels", ->
beforeEach ->
@CollaboratorHandler.getMemberIdsWithPrivilegeLevels = sinon.stub()
@@ -86,7 +87,7 @@ describe "CollaboratorsHandler", ->
@UserGetter.getUser.withArgs("read-write-ref-2").yields(null, { _id: "read-write-ref-2" })
@UserGetter.getUser.withArgs("doesnt-exist").yields(null, null)
@CollaboratorHandler.getMembersWithPrivilegeLevels @project_id, @callback
it "should return an array of members with their privilege levels", ->
@callback
.calledWith(null, [
@@ -96,7 +97,7 @@ describe "CollaboratorsHandler", ->
{ user: { _id: "read-write-ref-2" }, privilegeLevel: "readAndWrite" }
])
.should.equal true
describe "getMemberIdPrivilegeLevel", ->
beforeEach ->
@CollaboratorHandler.getMemberIdsWithPrivilegeLevels = sinon.stub()
@@ -116,7 +117,7 @@ describe "CollaboratorsHandler", ->
@CollaboratorHandler.getMemberIdPrivilegeLevel "member-id-3", @project_id, (error, level) ->
expect(level).to.equal false
done()
describe "isUserMemberOfProject", ->
beforeEach ->
@CollaboratorHandler.getMemberIdsWithPrivilegeLevels = sinon.stub()
@@ -128,7 +129,7 @@ describe "CollaboratorsHandler", ->
{ id: @user_id, privilegeLevel: "readAndWrite" }
])
@CollaboratorHandler.isUserMemberOfProject @user_id, @project_id, @callback
it "should return true and the privilegeLevel", ->
@callback
.calledWith(null, true, "readAndWrite")
@@ -140,12 +141,12 @@ describe "CollaboratorsHandler", ->
{ id: "not-the-user", privilegeLevel: "readOnly" }
])
@CollaboratorHandler.isUserMemberOfProject @user_id, @project_id, @callback
it "should return false", ->
@callback
.calledWith(null, false, null)
.should.equal true
describe "getProjectsUserIsCollaboratorOf", ->
beforeEach ->
@fields = "mock fields"
@@ -153,7 +154,7 @@ describe "CollaboratorsHandler", ->
@Project.find.withArgs({collaberator_refs:@user_id}, @fields).yields(null, ["mock-read-write-project-1", "mock-read-write-project-2"])
@Project.find.withArgs({readOnly_refs:@user_id}, @fields).yields(null, ["mock-read-only-project-1", "mock-read-only-project-2"])
@CollaboratorHandler.getProjectsUserIsCollaboratorOf @user_id, @fields, @callback
it "should call the callback with the projects", ->
@callback
.calledWith(null, ["mock-read-write-project-1", "mock-read-write-project-2"], ["mock-read-only-project-1", "mock-read-only-project-2"])
@@ -172,17 +173,15 @@ describe "CollaboratorsHandler", ->
"$pull":{collaberator_refs:@user_id, readOnly_refs:@user_id}
})
.should.equal true
describe "addUserToProject", ->
beforeEach ->
@Project.update = sinon.stub().callsArg(2)
@Project.findOne = sinon.stub().callsArgWith(2, null, @project = {})
@ProjectEntityHandler.flushProjectToThirdPartyDataStore = sinon.stub().callsArg(1)
@CollaboratorHandler.addEmailToProject = sinon.stub().callsArgWith(4, null, @user_id)
@UserGetter.getUser = sinon.stub().callsArgWith(2, null, @user = { _id: @user_id, email: @email })
@CollaboratorsEmailHandler.notifyUserOfProjectShare = sinon.stub()
@ContactManager.addContact = sinon.stub()
describe "as readOnly", ->
beforeEach ->
@CollaboratorHandler.addUserIdToProject @project_id, @adding_user_id, @user_id, "readOnly", @callback
@@ -195,22 +194,17 @@ describe "CollaboratorsHandler", ->
"$addToSet":{ readOnly_refs: @user_id }
})
.should.equal true
it "should flush the project to the TPDS", ->
@ProjectEntityHandler.flushProjectToThirdPartyDataStore
.calledWith(@project_id)
.should.equal true
it "should send an email to the shared-with user", ->
@CollaboratorsEmailHandler.notifyUserOfProjectShare
.calledWith(@project_id, @email)
.should.equal true
it "should add the user as a contact for the adding user", ->
@ContactManager.addContact
.calledWith(@adding_user_id, @user_id)
.should.equal true
describe "as readAndWrite", ->
beforeEach ->
@CollaboratorHandler.addUserIdToProject @project_id, @adding_user_id, @user_id, "readAndWrite", @callback
@@ -223,19 +217,19 @@ describe "CollaboratorsHandler", ->
"$addToSet":{ collaberator_refs: @user_id }
})
.should.equal true
it "should flush the project to the TPDS", ->
@ProjectEntityHandler.flushProjectToThirdPartyDataStore
.calledWith(@project_id)
.should.equal true
describe "with invalid privilegeLevel", ->
beforeEach ->
@CollaboratorHandler.addUserIdToProject @project_id, @adding_user_id, @user_id, "notValid", @callback
it "should call the callback with an error", ->
@callback.calledWith(new Error()).should.equal true
describe "when user already exists as a collaborator", ->
beforeEach ->
@project.collaberator_refs = [@user_id]
@@ -243,7 +237,7 @@ describe "CollaboratorsHandler", ->
it "should not add the user again", ->
@Project.update.called.should.equal false
describe "addEmailToProject", ->
beforeEach ->
@UserCreator.getUserOrCreateHoldingAccount = sinon.stub().callsArgWith(1, null, @user = {_id: @user_id})
@@ -252,27 +246,27 @@ describe "CollaboratorsHandler", ->
describe "with a valid email", ->
beforeEach ->
@CollaboratorHandler.addEmailToProject @project_id, @adding_user_id, (@email = "Joe@example.com"), (@privilegeLevel = "readAndWrite"), @callback
it "should get the user with the lowercased email", ->
@UserCreator.getUserOrCreateHoldingAccount
.calledWith(@email.toLowerCase())
.should.equal true
it "should add the user to the project by id", ->
@CollaboratorHandler.addUserIdToProject
.calledWith(@project_id, @adding_user_id, @user_id, @privilegeLevel)
.should.equal true
it "should return the callback with the user_id", ->
@callback.calledWith(null, @user_id).should.equal true
describe "with an invalid email", ->
beforeEach ->
@CollaboratorHandler.addEmailToProject @project_id, @adding_user_id, "not-and-email", (@privilegeLevel = "readAndWrite"), @callback
it "should call the callback with an error", ->
@callback.calledWith(new Error()).should.equal true
it "should not add any users to the proejct", ->
@CollaboratorHandler.addUserIdToProject.called.should.equal false
@@ -286,9 +280,67 @@ describe "CollaboratorsHandler", ->
)
@CollaboratorHandler.removeUserFromProject = sinon.stub().yields()
@CollaboratorHandler.removeUserFromAllProjets @user_id, done
it "should remove the user from each project", ->
for project_id in ["read-and-write-0", "read-and-write-1", "read-only-0", "read-only-1"]
@CollaboratorHandler.removeUserFromProject
.calledWith(project_id, @user_id)
.should.equal true
.should.equal true
describe 'getAllMembers', ->
beforeEach ->
@owning_user = {_id: 'owner-id', email: 'owner@example.com', features: {a: 1}}
@readwrite_user = {_id: 'readwrite-id', email: 'readwrite@example.com'}
@members = [
{user: @owning_user, privilegeLevel: "owner"},
{user: @readwrite_user, privilegeLevel: "readAndWrite"}
]
@CollaboratorHandler.getMembersWithPrivilegeLevels = sinon.stub().callsArgWith(1, null, @members)
@ProjectEditorHandler.buildOwnerAndMembersViews = sinon.stub().returns(@views = {
owner: @owning_user,
ownerFeatures: @owning_user.features,
members: [ {_id: @readwrite_user._id, email: @readwrite_user.email} ]
})
@callback = sinon.stub()
@CollaboratorHandler.getAllMembers @project_id, @callback
it 'should not produce an error', ->
@callback.callCount.should.equal 1
expect(@callback.firstCall.args[0]).to.equal null
it 'should produce a list of members', ->
@callback.callCount.should.equal 1
expect(@callback.firstCall.args[1]).to.deep.equal @views.members
it 'should call getMembersWithPrivileges', ->
@CollaboratorHandler.getMembersWithPrivilegeLevels.callCount.should.equal 1
@CollaboratorHandler.getMembersWithPrivilegeLevels.firstCall.args[0].should.equal @project_id
it 'should call ProjectEditorHandler.buildOwnerAndMembersViews', ->
@ProjectEditorHandler.buildOwnerAndMembersViews.callCount.should.equal 1
@ProjectEditorHandler.buildOwnerAndMembersViews.firstCall.args[0].should.equal @members
describe 'when getMembersWithPrivileges produces an error', ->
beforeEach ->
@CollaboratorHandler.getMembersWithPrivilegeLevels = sinon.stub().callsArgWith(1, new Error('woops'))
@ProjectEditorHandler.buildOwnerAndMembersViews = sinon.stub().returns(@views = {
owner: @owning_user,
ownerFeatures: @owning_user.features,
members: [ {_id: @readwrite_user._id, email: @readwrite_user.email} ]
})
@callback = sinon.stub()
@CollaboratorHandler.getAllMembers @project_id, @callback
it 'should produce an error', ->
@callback.callCount.should.equal 1
expect(@callback.firstCall.args[0]).to.not.equal null
expect(@callback.firstCall.args[0]).to.be.instanceof Error
it 'should call getMembersWithPrivileges', ->
@CollaboratorHandler.getMembersWithPrivilegeLevels.callCount.should.equal 1
@CollaboratorHandler.getMembersWithPrivilegeLevels.firstCall.args[0].should.equal @project_id
it 'should not call ProjectEditorHandler.buildOwnerAndMembersViews', ->
@ProjectEditorHandler.buildOwnerAndMembersViews.callCount.should.equal 0

View File

@@ -0,0 +1,574 @@
sinon = require('sinon')
chai = require('chai')
should = chai.should()
expect = chai.expect
modulePath = "../../../../app/js/Features/Collaborators/CollaboratorsInviteController.js"
SandboxedModule = require('sandboxed-module')
events = require "events"
MockRequest = require "../helpers/MockRequest"
MockResponse = require "../helpers/MockResponse"
ObjectId = require("mongojs").ObjectId
describe "CollaboratorsInviteController", ->
beforeEach ->
@CollaboratorsInviteController = SandboxedModule.require modulePath, requires:
"../Project/ProjectGetter": @ProjectGetter = {}
'../Subscription/LimitationsManager' : @LimitationsManager = {}
'../User/UserGetter': @UserGetter = {getUser: sinon.stub()}
"./CollaboratorsHandler": @CollaboratorsHandler = {}
"./CollaboratorsInviteHandler": @CollaboratorsInviteHandler = {}
'logger-sharelatex': @logger = {err: sinon.stub(), error: sinon.stub(), log: sinon.stub()}
"../Editor/EditorRealTimeController": @EditorRealTimeController = {emitToRoom: sinon.stub()}
"../Notifications/NotificationsBuilder": @NotificationsBuilder = {}
@res = new MockResponse()
@req = new MockRequest()
@project_id = "project-id-123"
@callback = sinon.stub()
describe 'getAllInvites', ->
beforeEach ->
@fakeInvites = [
{_id: ObjectId(), one: 1},
{_id: ObjectId(), two: 2}
]
@req.params =
Project_id: @project_id
@res.json = sinon.stub()
@next = sinon.stub()
describe 'when all goes well', ->
beforeEach ->
@CollaboratorsInviteHandler.getAllInvites = sinon.stub().callsArgWith(1, null, @fakeInvites)
@CollaboratorsInviteController.getAllInvites @req, @res, @next
it 'should not produce an error', ->
@next.callCount.should.equal 0
it 'should produce a list of invite objects', ->
@res.json.callCount.should.equal 1
@res.json.calledWith({invites: @fakeInvites}).should.equal true
it 'should have called CollaboratorsInviteHandler.getAllInvites', ->
@CollaboratorsInviteHandler.getAllInvites.callCount.should.equal 1
@CollaboratorsInviteHandler.getAllInvites.calledWith(@project_id).should.equal true
describe 'when CollaboratorsInviteHandler.getAllInvites produces an error', ->
beforeEach ->
@CollaboratorsInviteHandler.getAllInvites = sinon.stub().callsArgWith(1, new Error('woops'))
@CollaboratorsInviteController.getAllInvites @req, @res, @next
it 'should produce an error', ->
@next.callCount.should.equal 1
@next.firstCall.args[0].should.be.instanceof Error
describe 'inviteToProject', ->
beforeEach ->
@targetEmail = "user@example.com"
@req.params =
Project_id: @project_id
@current_user =
_id: @current_user_id = "current-user-id"
@req.session =
user: @current_user
@req.body =
email: @targetEmail
privileges: @privileges = "readAndWrite"
@res.json = sinon.stub()
@res.sendStatus = sinon.stub()
@invite = {
_id: ObjectId(),
token: "htnseuthaouse",
sendingUserId: @current_user_id,
projectId: @targetEmail,
targetEmail: 'user@example.com'
createdAt: new Date(),
}
@LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, null, true)
@CollaboratorsInviteHandler.inviteToProject = sinon.stub().callsArgWith(4, null, @invite)
@callback = sinon.stub()
@next = sinon.stub()
describe 'when all goes well', ->
beforeEach ->
@LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, null, true)
@CollaboratorsInviteController.inviteToProject @req, @res, @next
it 'should produce json response', ->
@res.json.callCount.should.equal 1
({invite: @invite}).should.deep.equal(@res.json.firstCall.args[0])
it 'should have called canAddXCollaborators', ->
@LimitationsManager.canAddXCollaborators.callCount.should.equal 1
@LimitationsManager.canAddXCollaborators.calledWith(@project_id).should.equal true
it 'should have called inviteToProject', ->
@CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 1
@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
describe 'when the user is not allowed to add more collaborators', ->
beforeEach ->
@LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, null, false)
@CollaboratorsInviteController.inviteToProject @req, @res, @next
it 'should produce json response without an invite', ->
@res.json.callCount.should.equal 1
({invite: null}).should.deep.equal(@res.json.firstCall.args[0])
it 'should not have called inviteToProject', ->
@CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 0
describe 'when canAddXCollaborators produces an error', ->
beforeEach ->
@err = new Error('woops')
@LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, @err)
@CollaboratorsInviteController.inviteToProject @req, @res, @next
it 'should call next with an error', ->
@next.callCount.should.equal 1
@next.calledWith(@err).should.equal true
it 'should not have called inviteToProject', ->
@CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 0
describe 'when inviteToProject produces an error', ->
beforeEach ->
@err = new Error('woops')
@CollaboratorsInviteHandler.inviteToProject = sinon.stub().callsArgWith(4, @err)
@CollaboratorsInviteController.inviteToProject @req, @res, @next
it 'should call next with an error', ->
@next.callCount.should.equal 1
@next.calledWith(@err).should.equal true
it 'should have called canAddXCollaborators', ->
@LimitationsManager.canAddXCollaborators.callCount.should.equal 1
@LimitationsManager.canAddXCollaborators.calledWith(@project_id).should.equal true
it 'should have called inviteToProject', ->
@CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 1
@CollaboratorsInviteHandler.inviteToProject.calledWith(@project_id,@current_user,@targetEmail,@privileges).should.equal true
describe "viewInvite", ->
beforeEach ->
@token = "some-opaque-token"
@req.params =
Project_id: @project_id
token: @token
@req.session =
user: _id: @current_user_id = "current-user-id"
@res.render = sinon.stub()
@res.redirect = sinon.stub()
@res.sendStatus = sinon.stub()
@invite = {
_id: ObjectId(),
token: @token,
sendingUserId: ObjectId(),
projectId: @project_id,
targetEmail: 'user@example.com'
createdAt: new Date(),
}
@fakeProject =
_id: @project_id
name: "some project"
owner_ref: @invite.sendingUserId
collaberator_refs: []
readOnly_refs: []
@owner =
_id: @fakeProject.owner_ref
first_name: "John"
last_name: "Doe"
email: "john@example.com"
@CollaboratorsHandler.isUserMemberOfProject = sinon.stub().callsArgWith(2, null, false, null)
@CollaboratorsInviteHandler.getInviteByToken = sinon.stub().callsArgWith(2, null, @invite)
@ProjectGetter.getProject = sinon.stub().callsArgWith(2, null, @fakeProject)
@UserGetter.getUser.callsArgWith(2, null, @owner)
@callback = sinon.stub()
@next = sinon.stub()
describe 'when the token is valid', ->
beforeEach ->
@CollaboratorsInviteController.viewInvite @req, @res, @next
it 'should render the view template', ->
@res.render.callCount.should.equal 1
@res.render.calledWith('project/invite/show').should.equal true
it 'should not call next', ->
@next.callCount.should.equal 0
it 'should call CollaboratorsHandler.isUserMemberOfProject', ->
@CollaboratorsHandler.isUserMemberOfProject.callCount.should.equal 1
@CollaboratorsHandler.isUserMemberOfProject.calledWith(@current_user_id, @project_id).should.equal true
it 'should call getInviteByToken', ->
@CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 1
@CollaboratorsInviteHandler.getInviteByToken.calledWith(@fakeProject._id, @invite.token).should.equal true
it 'should call User.getUser', ->
@UserGetter.getUser.callCount.should.equal 1
@UserGetter.getUser.calledWith({_id: @fakeProject.owner_ref}).should.equal true
it 'should call ProjectGetter.getProject', ->
@ProjectGetter.getProject.callCount.should.equal 1
@ProjectGetter.getProject.calledWith(@project_id).should.equal true
describe 'when user is already a member of the project', ->
beforeEach ->
@CollaboratorsHandler.isUserMemberOfProject = sinon.stub().callsArgWith(2, null, true, null)
@CollaboratorsInviteController.viewInvite @req, @res, @next
it 'should redirect to the project page', ->
@res.redirect.callCount.should.equal 1
@res.redirect.calledWith("/project/#{@project_id}").should.equal true
it 'should not call next with an error', ->
@next.callCount.should.equal 0
it 'should call CollaboratorsHandler.isUserMemberOfProject', ->
@CollaboratorsHandler.isUserMemberOfProject.callCount.should.equal 1
@CollaboratorsHandler.isUserMemberOfProject.calledWith(@current_user_id, @project_id).should.equal true
it 'should not call getInviteByToken', ->
@CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 0
it 'should not call User.getUser', ->
@UserGetter.getUser.callCount.should.equal 0
it 'should not call ProjectGetter.getProject', ->
@ProjectGetter.getProject.callCount.should.equal 0
describe 'when isUserMemberOfProject produces an error', ->
beforeEach ->
@CollaboratorsHandler.isUserMemberOfProject = sinon.stub().callsArgWith(2, new Error('woops'))
@CollaboratorsInviteController.viewInvite @req, @res, @next
it 'should call next with an error', ->
@next.callCount.should.equal 1
expect(@next.firstCall.args[0]).to.be.instanceof Error
it 'should call CollaboratorsHandler.isUserMemberOfProject', ->
@CollaboratorsHandler.isUserMemberOfProject.callCount.should.equal 1
@CollaboratorsHandler.isUserMemberOfProject.calledWith(@current_user_id, @project_id).should.equal true
it 'should not call getInviteByToken', ->
@CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 0
it 'should not call User.getUser', ->
@UserGetter.getUser.callCount.should.equal 0
it 'should not call ProjectGetter.getProject', ->
@ProjectGetter.getProject.callCount.should.equal 0
describe 'when the getInviteByToken produces an error', ->
beforeEach ->
@err = new Error('woops')
@CollaboratorsInviteHandler.getInviteByToken.callsArgWith(2, @err)
@CollaboratorsInviteController.viewInvite @req, @res, @next
it 'should call next with the error', ->
@next.callCount.should.equal 1
@next.calledWith(@err).should.equal true
it 'should call CollaboratorsHandler.isUserMemberOfProject', ->
@CollaboratorsHandler.isUserMemberOfProject.callCount.should.equal 1
@CollaboratorsHandler.isUserMemberOfProject.calledWith(@current_user_id, @project_id).should.equal true
it 'should call getInviteByToken', ->
@CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 1
@CollaboratorsHandler.isUserMemberOfProject.calledWith(@current_user_id, @project_id).should.equal true
it 'should not call User.getUser', ->
@UserGetter.getUser.callCount.should.equal 0
it 'should not call ProjectGetter.getProject', ->
@ProjectGetter.getProject.callCount.should.equal 0
describe 'when the getInviteByToken does not produce an invite', ->
beforeEach ->
@CollaboratorsInviteHandler.getInviteByToken.callsArgWith(2, null, null)
@CollaboratorsInviteController.viewInvite @req, @res, @next
it 'should render the not-valid view template', ->
@res.render.callCount.should.equal 1
@res.render.calledWith('project/invite/not-valid').should.equal true
it 'should not call next', ->
@next.callCount.should.equal 0
it 'should call CollaboratorsHandler.isUserMemberOfProject', ->
@CollaboratorsHandler.isUserMemberOfProject.callCount.should.equal 1
@CollaboratorsHandler.isUserMemberOfProject.calledWith(@current_user_id, @project_id).should.equal true
it 'should call getInviteByToken', ->
@CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 1
@CollaboratorsHandler.isUserMemberOfProject.calledWith(@current_user_id, @project_id).should.equal true
it 'should not call User.getUser', ->
@UserGetter.getUser.callCount.should.equal 0
it 'should not call ProjectGetter.getProject', ->
@ProjectGetter.getProject.callCount.should.equal 0
describe 'when User.getUser produces an error', ->
beforeEach ->
@UserGetter.getUser.callsArgWith(2, new Error('woops'))
@CollaboratorsInviteController.viewInvite @req, @res, @next
it 'should produce an error', ->
@next.callCount.should.equal 1
expect(@next.firstCall.args[0]).to.be.instanceof Error
it 'should call CollaboratorsHandler.isUserMemberOfProject', ->
@CollaboratorsHandler.isUserMemberOfProject.callCount.should.equal 1
@CollaboratorsHandler.isUserMemberOfProject.calledWith(@current_user_id, @project_id).should.equal true
it 'should call getInviteByToken', ->
@CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 1
it 'should call User.getUser', ->
@UserGetter.getUser.callCount.should.equal 1
@UserGetter.getUser.calledWith({_id: @fakeProject.owner_ref}).should.equal true
it 'should not call ProjectGetter.getProject', ->
@ProjectGetter.getProject.callCount.should.equal 0
describe 'when User.getUser does not find a user', ->
beforeEach ->
@UserGetter.getUser.callsArgWith(2, null, null)
@CollaboratorsInviteController.viewInvite @req, @res, @next
it 'should render the not-valid view template', ->
@res.render.callCount.should.equal 1
@res.render.calledWith('project/invite/not-valid').should.equal true
it 'should not call next', ->
@next.callCount.should.equal 0
it 'should call CollaboratorsHandler.isUserMemberOfProject', ->
@CollaboratorsHandler.isUserMemberOfProject.callCount.should.equal 1
@CollaboratorsHandler.isUserMemberOfProject.calledWith(@current_user_id, @project_id).should.equal true
it 'should call getInviteByToken', ->
@CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 1
it 'should call User.getUser', ->
@UserGetter.getUser.callCount.should.equal 1
@UserGetter.getUser.calledWith({_id: @fakeProject.owner_ref}).should.equal true
it 'should not call ProjectGetter.getProject', ->
@ProjectGetter.getProject.callCount.should.equal 0
describe 'when getProject produces an error', ->
beforeEach ->
@ProjectGetter.getProject.callsArgWith(2, new Error('woops'))
@CollaboratorsInviteController.viewInvite @req, @res, @next
it 'should produce an error', ->
@next.callCount.should.equal 1
expect(@next.firstCall.args[0]).to.be.instanceof Error
it 'should call CollaboratorsHandler.isUserMemberOfProject', ->
@CollaboratorsHandler.isUserMemberOfProject.callCount.should.equal 1
@CollaboratorsHandler.isUserMemberOfProject.calledWith(@current_user_id, @project_id).should.equal true
it 'should call getInviteByToken', ->
@CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 1
it 'should call User.getUser', ->
@UserGetter.getUser.callCount.should.equal 1
@UserGetter.getUser.calledWith({_id: @fakeProject.owner_ref}).should.equal true
it 'should call ProjectGetter.getProject', ->
@ProjectGetter.getProject.callCount.should.equal 1
describe 'when Project.getUser does not find a user', ->
beforeEach ->
@ProjectGetter.getProject.callsArgWith(2, null, null)
@CollaboratorsInviteController.viewInvite @req, @res, @next
it 'should render the not-valid view template', ->
@res.render.callCount.should.equal 1
@res.render.calledWith('project/invite/not-valid').should.equal true
it 'should not call next', ->
@next.callCount.should.equal 0
it 'should call CollaboratorsHandler.isUserMemberOfProject', ->
@CollaboratorsHandler.isUserMemberOfProject.callCount.should.equal 1
@CollaboratorsHandler.isUserMemberOfProject.calledWith(@current_user_id, @project_id).should.equal true
it 'should call getInviteByToken', ->
@CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 1
it 'should call getUser', ->
@UserGetter.getUser.callCount.should.equal 1
@UserGetter.getUser.calledWith({_id: @fakeProject.owner_ref}).should.equal true
it 'should call ProjectGetter.getProject', ->
@ProjectGetter.getProject.callCount.should.equal 1
describe "resendInvite", ->
beforeEach ->
@req.params =
Project_id: @project_id
invite_id: @invite_id = "thuseoautoh"
@req.session =
user: _id: @current_user_id = "current-user-id"
@res.render = sinon.stub()
@res.sendStatus = sinon.stub()
@CollaboratorsInviteHandler.resendInvite = sinon.stub().callsArgWith(3, null)
@callback = sinon.stub()
@next = sinon.stub()
describe 'when resendInvite does not produce an error', ->
beforeEach ->
@CollaboratorsInviteController.resendInvite @req, @res, @next
it 'should produce a 201 response', ->
@res.sendStatus.callCount.should.equal 1
@res.sendStatus.calledWith(201).should.equal true
it 'should have called resendInvite', ->
@CollaboratorsInviteHandler.resendInvite.callCount.should.equal 1
describe 'when resendInvite produces an error', ->
beforeEach ->
@err = new Error('woops')
@CollaboratorsInviteHandler.resendInvite = sinon.stub().callsArgWith(3, @err)
@CollaboratorsInviteController.resendInvite @req, @res, @next
it 'should not produce a 201 response', ->
@res.sendStatus.callCount.should.equal 0
it 'should call next with the error', ->
@next.callCount.should.equal 1
@next.calledWith(@err).should.equal true
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: @current_user
@res.render = sinon.stub()
@res.sendStatus = sinon.stub()
@CollaboratorsInviteHandler.revokeInvite = sinon.stub().callsArgWith(2, null)
@callback = sinon.stub()
@next = sinon.stub()
describe 'when revokeInvite does not produce an error', ->
beforeEach ->
@CollaboratorsInviteController.revokeInvite @req, @res, @next
it 'should produce a 201 response', ->
@res.sendStatus.callCount.should.equal 1
@res.sendStatus.calledWith(201).should.equal true
it 'should have called revokeInvite', ->
@CollaboratorsInviteHandler.revokeInvite.callCount.should.equal 1
it 'should have called emitToRoom', ->
@EditorRealTimeController.emitToRoom.callCount.should.equal 1
@EditorRealTimeController.emitToRoom.calledWith(@project_id, 'project:membership:changed').should.equal true
describe 'when revokeInvite produces an error', ->
beforeEach ->
@err = new Error('woops')
@CollaboratorsInviteHandler.revokeInvite = sinon.stub().callsArgWith(2, @err)
@CollaboratorsInviteController.revokeInvite @req, @res, @next
it 'should not produce a 201 response', ->
@res.sendStatus.callCount.should.equal 0
it 'should call next with the error', ->
@next.callCount.should.equal 1
@next.calledWith(@err).should.equal true
it 'should have called revokeInvite', ->
@CollaboratorsInviteHandler.revokeInvite.callCount.should.equal 1
describe "acceptInvite", ->
beforeEach ->
@req.params =
Project_id: @project_id
invite_id: @invite_id = "thuseoautoh"
@req.session =
user: _id: @current_user_id = "current-user-id"
@req.body =
token: "thsueothaueotauahsuet"
@res.render = sinon.stub()
@res.redirect = sinon.stub()
@CollaboratorsInviteHandler.acceptInvite = sinon.stub().callsArgWith(4, null)
@callback = sinon.stub()
@next = sinon.stub()
describe 'when acceptInvite does not produce an error', ->
beforeEach ->
@CollaboratorsInviteController.acceptInvite @req, @res, @next
it 'should redirect to project page', () ->
@res.redirect.callCount.should.equal 1
@res.redirect.calledWith("/project/#{@project_id}").should.equal true
it 'should have called acceptInvite', ->
@CollaboratorsInviteHandler.acceptInvite.callCount.should.equal 1
it 'should have called emitToRoom', ->
@EditorRealTimeController.emitToRoom.callCount.should.equal 1
@EditorRealTimeController.emitToRoom.calledWith(@project_id, 'project:membership:changed').should.equal true
describe 'when revokeInvite produces an error', ->
beforeEach ->
@err = new Error('woops')
@CollaboratorsInviteHandler.acceptInvite = sinon.stub().callsArgWith(4, @err)
@CollaboratorsInviteController.acceptInvite @req, @res, @next
it 'should not redirect to project page', ->
@res.redirect.callCount.should.equal 0
it 'should call next with the error', ->
@next.callCount.should.equal 1
@next.calledWith(@err).should.equal true
it 'should have called acceptInvite', ->
@CollaboratorsInviteHandler.acceptInvite.callCount.should.equal 1

View File

@@ -0,0 +1,723 @@
sinon = require('sinon')
chai = require('chai')
should = chai.should()
expect = chai.expect
modulePath = "../../../../app/js/Features/Collaborators/CollaboratorsInviteHandler.js"
SandboxedModule = require('sandboxed-module')
events = require "events"
ObjectId = require("mongojs").ObjectId
Crypto = require('crypto')
describe "CollaboratorsInviteHandler", ->
beforeEach ->
@ProjectInvite = class ProjectInvite
constructor: (options={}) ->
this._id = ObjectId()
for k,v of options
this[k] = v
this
save: sinon.stub()
@findOne: sinon.stub()
@find: sinon.stub()
@remove: sinon.stub()
@count: sinon.stub()
@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()}
'../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 =
_id: @userId
email: 'someone@example.com'
@inviteId = ObjectId()
@token = 'hnhteaosuhtaeosuahs'
@privileges = "readAndWrite"
@fakeInvite =
_id: @inviteId
email: @email
token: @token
sendingUserId: @sendingUserId
projectId: @projectId
privileges: @privileges
createdAt: new Date()
describe 'getInviteCount', ->
beforeEach ->
@ProjectInvite.count.callsArgWith(1, null, 2)
@call = (callback) =>
@CollaboratorsInviteHandler.getInviteCount @projectId, callback
it 'should not produce an error', (done) ->
@call (err, invites) =>
expect(err).to.not.be.instanceof Error
expect(err).to.be.oneOf [null, undefined]
done()
it 'should produce the count of documents', (done) ->
@call (err, count) =>
expect(count).to.equal 2
done()
describe 'when model.count produces an error', ->
beforeEach ->
@ProjectInvite.count.callsArgWith(1, new Error('woops'))
it 'should produce an error', (done) ->
@call (err, count) =>
expect(err).to.be.instanceof Error
done()
describe 'getAllInvites', ->
beforeEach ->
@fakeInvites = [
{_id: ObjectId(), one: 1},
{_id: ObjectId(), two: 2}
]
@ProjectInvite.find.callsArgWith(1, null, @fakeInvites)
@call = (callback) =>
@CollaboratorsInviteHandler.getAllInvites @projectId, callback
describe 'when all goes well', ->
beforeEach ->
it 'should not produce an error', (done) ->
@call (err, invites) =>
expect(err).to.not.be.instanceof Error
expect(err).to.be.oneOf [null, undefined]
done()
it 'should produce a list of invite objects', (done) ->
@call (err, invites) =>
expect(invites).to.not.be.oneOf [null, undefined]
expect(invites).to.deep.equal @fakeInvites
done()
it 'should have called ProjectInvite.find', (done) ->
@call (err, invites) =>
@ProjectInvite.find.callCount.should.equal 1
@ProjectInvite.find.calledWith({projectId: @projectId}).should.equal true
done()
describe 'when ProjectInvite.find produces an error', ->
beforeEach ->
@ProjectInvite.find.callsArgWith(1, new Error('woops'))
it 'should produce an error', (done) ->
@call (err, invites) =>
expect(err).to.be.instanceof Error
done()
describe 'inviteToProject', ->
beforeEach ->
@ProjectInvite::save = sinon.spy (cb) -> cb(null, this)
@randomBytesSpy = sinon.spy(@Crypto, 'randomBytes')
@CollaboratorsInviteHandler._sendMessages = sinon.stub().callsArgWith(3, null)
@call = (callback) =>
@CollaboratorsInviteHandler.inviteToProject @projectId, @sendingUser, @email, @privileges, callback
afterEach ->
@randomBytesSpy.restore()
describe 'when all goes well', ->
beforeEach ->
it 'should not produce an error', (done) ->
@call (err, invite) =>
expect(err).to.not.be.instanceof Error
expect(err).to.be.oneOf [null, undefined]
done()
it 'should produce the invite object', (done) ->
@call (err, invite) =>
expect(invite).to.not.equal null
expect(invite).to.not.equal undefined
expect(invite).to.be.instanceof Object
expect(invite).to.have.all.keys ['_id', 'email', 'token', 'sendingUserId', 'projectId', 'privileges']
done()
it 'should have generated a random token', (done) ->
@call (err, invite) =>
@randomBytesSpy.callCount.should.equal 1
done()
it 'should have called ProjectInvite.save', (done) ->
@call (err, invite) =>
@ProjectInvite::save.callCount.should.equal 1
done()
it 'should have called _sendMessages', (done) ->
@call (err, invite) =>
@CollaboratorsInviteHandler._sendMessages.callCount.should.equal 1
@CollaboratorsInviteHandler._sendMessages.calledWith(@projectId, @sendingUser).should.equal true
done()
describe 'when saving model produces an error', ->
beforeEach ->
@ProjectInvite::save = sinon.spy (cb) -> cb(new Error('woops'), this)
it 'should produce an error', (done) ->
@call (err, invite) =>
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
describe 'when all goes well', ->
beforeEach ->
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 ProjectInvite.remove', (done) ->
@call (err) =>
@ProjectInvite.remove.callCount.should.equal 1
@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 ->
@ProjectInvite.remove.callsArgWith(1, new Error('woops'))
it 'should produce an error', (done) ->
@call (err) =>
expect(err).to.be.instanceof Error
done()
describe 'resendInvite', ->
beforeEach ->
@ProjectInvite.findOne.callsArgWith(1, null, @fakeInvite)
@CollaboratorsInviteHandler._sendMessages = sinon.stub().callsArgWith(3, null)
@call = (callback) =>
@CollaboratorsInviteHandler.resendInvite @projectId, @sendingUser, @inviteId, callback
describe 'when all goes well', ->
beforeEach ->
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 ProjectInvite.findOne', (done) ->
@call (err, invite) =>
@ProjectInvite.findOne.callCount.should.equal 1
@ProjectInvite.findOne.calledWith({_id: @inviteId, projectId: @projectId}).should.equal true
done()
it 'should have called _sendMessages', (done) ->
@call (err, invite) =>
@CollaboratorsInviteHandler._sendMessages.callCount.should.equal 1
@CollaboratorsInviteHandler._sendMessages.calledWith(@projectId, @sendingUser, @fakeInvite).should.equal true
done()
describe 'when findOne produces an error', ->
beforeEach ->
@ProjectInvite.findOne.callsArgWith(1, new Error('woops'))
it 'should produce an error', (done) ->
@call (err, invite) =>
expect(err).to.be.instanceof Error
done()
it 'should not have called _sendMessages', (done) ->
@call (err, invite) =>
@CollaboratorsInviteHandler._sendMessages.callCount.should.equal 0
done()
describe 'when findOne does not find an invite', ->
beforeEach ->
@ProjectInvite.findOne.callsArgWith(1, null, null)
it 'should not produce an error', (done) ->
@call (err, invite) =>
expect(err).to.not.be.instanceof Error
expect(err).to.be.oneOf [null, undefined]
done()
it 'should not have called _sendMessages', (done) ->
@call (err, invite) =>
@CollaboratorsInviteHandler._sendMessages.callCount.should.equal 0
done()
describe 'getInviteByToken', ->
beforeEach ->
@ProjectInvite.findOne.callsArgWith(1, null, @fakeInvite)
@call = (callback) =>
@CollaboratorsInviteHandler.getInviteByToken @projectId, @token, callback
describe 'when all goes well', ->
beforeEach ->
it 'should not produce an error', (done) ->
@call (err, invite) =>
expect(err).to.not.be.instanceof Error
expect(err).to.be.oneOf [null, undefined]
done()
it 'should produce the invite object', (done) ->
@call (err, invite) =>
expect(invite).to.deep.equal @fakeInvite
done()
it 'should call ProjectInvite.findOne', (done) ->
@call (err, invite) =>
@ProjectInvite.findOne.callCount.should.equal 1
@ProjectInvite.findOne.calledWith({projectId: @projectId, token: @token}).should.equal true
done()
describe 'when findOne produces an error', ->
beforeEach ->
@ProjectInvite.findOne.callsArgWith(1, new Error('woops'))
it 'should produce an error', (done) ->
@call (err, invite) =>
expect(err).to.be.instanceof Error
done()
describe 'when findOne does not find an invite', ->
beforeEach ->
@ProjectInvite.findOne.callsArgWith(1, null, null)
it 'should not produce an error', (done) ->
@call (err, invite) =>
expect(err).to.not.be.instanceof Error
expect(err).to.be.oneOf [null, undefined]
done()
it 'should not produce an invite object', (done) ->
@call (err, invite) =>
expect(invite).to.not.be.instanceof Error
expect(invite).to.be.oneOf [null, undefined]
done()
describe 'acceptInvite', ->
beforeEach ->
@fakeProject =
_id: @projectId
collaberator_refs: []
readOnly_refs: []
@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
afterEach ->
@_getInviteByToken.restore()
describe 'when all goes well', ->
beforeEach ->
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 have called getInviteByToken', (done) ->
@call (err) =>
@_getInviteByToken.callCount.should.equal 1
@_getInviteByToken.calledWith(@projectId, @token).should.equal true
done()
it 'should have called CollaboratorsHandler.addUserIdToProject', (done) ->
@call (err) =>
@CollaboratorsHandler.addUserIdToProject.callCount.should.equal 1
@CollaboratorsHandler.addUserIdToProject.calledWith(@projectId, @sendingUserId, @userId, @fakeInvite.privileges).should.equal true
done()
it 'should have called ProjectInvite.remove', (done) ->
@call (err) =>
@ProjectInvite.remove.callCount.should.equal 1
@ProjectInvite.remove.calledWith({_id: @inviteId}).should.equal true
done()
describe 'when the invite is for readOnly access', ->
beforeEach ->
@fakeInvite.privileges = 'readOnly'
@_getInviteByToken.callsArgWith(2, null, @fakeInvite)
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 have called CollaboratorsHandler.addUserIdToProject', (done) ->
@call (err) =>
@CollaboratorsHandler.addUserIdToProject.callCount.should.equal 1
@CollaboratorsHandler.addUserIdToProject.calledWith(@projectId, @sendingUserId, @userId, @fakeInvite.privileges).should.equal true
done()
describe 'when getInviteByToken does not find an invite', ->
beforeEach ->
@_getInviteByToken.callsArgWith(2, null, null)
it 'should produce an error', (done) ->
@call (err) =>
expect(err).to.be.instanceof Error
expect(err.name).to.equal "NotFoundError"
done()
it 'should have called getInviteByToken', (done) ->
@call (err) =>
@_getInviteByToken.callCount.should.equal 1
@_getInviteByToken.calledWith(@projectId, @token).should.equal true
done()
it 'should not have called CollaboratorsHandler.addUserIdToProject', (done) ->
@call (err) =>
@CollaboratorsHandler.addUserIdToProject.callCount.should.equal 0
done()
it 'should not have called ProjectInvite.remove', (done) ->
@call (err) =>
@ProjectInvite.remove.callCount.should.equal 0
done()
describe 'when getInviteByToken produces an error', ->
beforeEach ->
@_getInviteByToken.callsArgWith(2, new Error('woops'))
it 'should produce an error', (done) ->
@call (err) =>
expect(err).to.be.instanceof Error
done()
it 'should have called getInviteByToken', (done) ->
@call (err) =>
@_getInviteByToken.callCount.should.equal 1
@_getInviteByToken.calledWith(@projectId, @token).should.equal true
done()
it 'should not have called CollaboratorsHandler.addUserIdToProject', (done) ->
@call (err) =>
@CollaboratorsHandler.addUserIdToProject.callCount.should.equal 0
done()
it 'should not have called ProjectInvite.remove', (done) ->
@call (err) =>
@ProjectInvite.remove.callCount.should.equal 0
done()
describe 'when addUserIdToProject produces an error', ->
beforeEach ->
@CollaboratorsHandler.addUserIdToProject.callsArgWith(4, new Error('woops'))
it 'should produce an error', (done) ->
@call (err) =>
expect(err).to.be.instanceof Error
done()
it 'should have called getInviteByToken', (done) ->
@call (err) =>
@_getInviteByToken.callCount.should.equal 1
@_getInviteByToken.calledWith(@projectId, @token).should.equal true
done()
it 'should have called CollaboratorsHandler.addUserIdToProject', (done) ->
@call (err) =>
@CollaboratorsHandler.addUserIdToProject.callCount.should.equal 1
@CollaboratorsHandler.addUserIdToProject.calledWith(@projectId, @sendingUserId, @userId, @fakeInvite.privileges).should.equal true
done()
it 'should not have called ProjectInvite.remove', (done) ->
@call (err) =>
@ProjectInvite.remove.callCount.should.equal 0
done()
describe 'when ProjectInvite.remove produces an error', ->
beforeEach ->
@ProjectInvite.remove.callsArgWith(1, new Error('woops'))
it 'should produce an error', (done) ->
@call (err) =>
expect(err).to.be.instanceof Error
done()
it 'should have called getInviteByToken', (done) ->
@call (err) =>
@_getInviteByToken.callCount.should.equal 1
@_getInviteByToken.calledWith(@projectId, @token).should.equal true
done()
it 'should have called CollaboratorsHandler.addUserIdToProject', (done) ->
@call (err) =>
@CollaboratorsHandler.addUserIdToProject.callCount.should.equal 1
@CollaboratorsHandler.addUserIdToProject.calledWith(@projectId, @sendingUserId, @userId, @fakeInvite.privileges).should.equal true
done()
it 'should have called ProjectInvite.remove', (done) ->
@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()

View File

@@ -17,7 +17,8 @@ describe "EditorHttpController", ->
"./EditorController": @EditorController = {}
'../../infrastructure/Metrics': @Metrics = {inc: sinon.stub()}
"../Collaborators/CollaboratorsHandler": @CollaboratorsHandler = {}
"../Collaborators/CollaboratorsInviteHandler": @CollaboratorsInviteHandler = {}
@project_id = "mock-project-id"
@doc_id = "mock-doc-id"
@user_id = "mock-user-id"
@@ -102,9 +103,14 @@ describe "EditorHttpController", ->
_id: @project_id
owner:{_id:"something"}
view: true
@invites = [
{_id: "invite_one", email: "user-one@example.com", privileges: "readOnly", projectId: @project._id}
{_id: "invite_two", email: "user-two@example.com", privileges: "readOnly", projectId: @project._id}
]
@ProjectEditorHandler.buildProjectModelView = sinon.stub().returns(@projectModelView)
@ProjectGetter.getProjectWithoutDocLines = sinon.stub().callsArgWith(1, null, @project)
@CollaboratorsHandler.getMembersWithPrivilegeLevels = sinon.stub().callsArgWith(1, null, @members)
@CollaboratorsInviteHandler.getAllInvites = sinon.stub().callsArgWith(1, null, @invites)
@UserGetter.getUser = sinon.stub().callsArgWith(2, null, @user)
describe "when authorized", ->
@@ -133,6 +139,11 @@ describe "EditorHttpController", ->
.calledWith(@user_id, @project_id)
.should.equal true
it 'should include the invites', ->
@CollaboratorsInviteHandler.getAllInvites
.calledWith(@project._id)
.should.equal true
it "should return the project model view, privilege level and protocol version", ->
@callback.calledWith(null, @projectModelView, "owner").should.equal true

View File

@@ -60,12 +60,61 @@ describe 'NotificationsHandler', ->
@key = "some key here"
@messageOpts = {value:12344}
@templateKey = "renderThisHtml"
@expiry = null
@forceCreate = false
it "should post the message over", (done)->
@handler.createNotification user_id, @key, @templateKey, @messageOpts, =>
@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}
assert.deepEqual(args.json, expectedJson)
done()
done()
describe 'when expiry date is supplied', ->
beforeEach ->
@key = "some key here"
@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, @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, expires: @expiry}
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"
it 'should send a delete request when a delete has been received to mark a notification', (done)->
@handler.markAsReadByKeyOnly @key, =>
opts =
uri: "#{notificationUrl}/key/#{@key}"
timeout:1000
method: "DELETE"
@request.calledWith(opts).should.equal true
done()

View File

@@ -1,4 +1,5 @@
chai = require('chai')
expect = chai.expect
should = chai.should()
modulePath = "../../../../app/js/Features/Project/ProjectEditorHandler"
@@ -67,12 +68,16 @@ describe "ProjectEditorHandler", ->
},
privilegeLevel: "readAndWrite"
}]
@invites = [
{_id: "invite_one", email: "user-one@example.com", privileges: "readOnly", projectId: @project._id}
{_id: "invite_two", email: "user-two@example.com", privileges: "readOnly", projectId: @project._id}
]
@handler = SandboxedModule.require modulePath
describe "buildProjectModelView", ->
describe "with owner and members included", ->
beforeEach ->
@result = @handler.buildProjectModelView @project, @members
@result = @handler.buildProjectModelView @project, @members, @invites
it "should include the id", ->
should.exist @result._id
@@ -101,7 +106,7 @@ describe "ProjectEditorHandler", ->
it "should include the deletedDocs", ->
should.exist @result.deletedDocs
@result.deletedDocs.should.equal @project.deletedDocs
it "should gather readOnly_refs and collaberators_refs into a list of members", ->
findMember = (id) =>
for member in @result.members
@@ -144,6 +149,10 @@ describe "ProjectEditorHandler", ->
@result.rootFolder[0].folders[0].docs[0].name.should.equal "main.tex"
should.not.exist @result.rootFolder[0].folders[0].docs[0].lines
it 'should include invites', ->
should.exist @result.invites
@result.invites.should.deep.equal @invites
describe "deletedByExternalDataSource", ->
it "should set the deletedByExternalDataSource flag to false when it is not there", ->
@@ -168,10 +177,46 @@ describe "ProjectEditorHandler", ->
compileGroup:"priority"
compileTimeout: 96
@result = @handler.buildProjectModelView @project, @members
it "should copy the owner features to the project", ->
@result.features.versioning.should.equal @owner.features.versioning
@result.features.collaborators.should.equal @owner.features.collaborators
@result.features.compileGroup.should.equal @owner.features.compileGroup
@result.features.compileTimeout.should.equal @owner.features.compileTimeout
describe 'buildOwnerAndMembersViews', ->
beforeEach ->
@owner.features =
versioning: true
collaborators: 3
compileGroup:"priority"
compileTimeout: 22
@result = @handler.buildOwnerAndMembersViews @members
it 'should produce an object with owner, ownerFeatures and members keys', ->
expect(@result).to.have.all.keys ['owner', 'ownerFeatures', 'members']
it 'should separate the owner from the members', ->
@result.members.length.should.equal(@members.length-1)
expect(@result.owner._id).to.equal @owner._id
expect(@result.owner.email).to.equal @owner.email
expect(@result.members.filter((m) => m._id == @owner._id).length).to.equal 0
it 'should extract the ownerFeatures from the owner object', ->
expect(@result.ownerFeatures).to.deep.equal @owner.features
describe 'when there is no owner', ->
beforeEach ->
# remove the owner from members list
@membersWithoutOwner = @members.filter((m) => m.user._id != @owner._id)
@result = @handler.buildOwnerAndMembersViews @membersWithoutOwner
it 'should produce an object with owner, ownerFeatures and members keys', ->
expect(@result).to.have.all.keys ['owner', 'ownerFeatures', 'members']
it 'should not separate out an owner', ->
@result.members.length.should.equal @membersWithoutOwner.length
expect(@result.owner).to.equal null
it 'should not extract the ownerFeatures from the owner object', ->
expect(@result.ownerFeatures).to.equal null

View File

@@ -30,6 +30,7 @@ describe "LimitationsManager", ->
'./SubscriptionLocator':@SubscriptionLocator
'settings-sharelatex' : @Settings = {}
"../Collaborators/CollaboratorsHandler": @CollaboratorsHandler = {}
"../Collaborators/CollaboratorsInviteHandler": @CollaboratorsInviteHandler = {}
'logger-sharelatex':log:->
describe "allowedNumberOfCollaboratorsInProject", ->
@@ -56,6 +57,7 @@ describe "LimitationsManager", ->
describe "canAddXCollaborators", ->
beforeEach ->
@CollaboratorsHandler.getCollaboratorCount = (project_id, callback) => callback(null, @current_number)
@CollaboratorsInviteHandler.getInviteCount = (project_id, callback) => callback(null, @invite_count)
sinon.stub @LimitationsManager,
"allowedNumberOfCollaboratorsInProject",
(project_id, callback) => callback(null, @allowed_number)
@@ -65,6 +67,17 @@ describe "LimitationsManager", ->
beforeEach ->
@current_number = 1
@allowed_number = 2
@invite_count = 0
@LimitationsManager.canAddXCollaborators(@project_id, 1, @callback)
it "should return true", ->
@callback.calledWith(null, true).should.equal true
describe "when the project has fewer collaborators and invites than allowed", ->
beforeEach ->
@current_number = 1
@allowed_number = 4
@invite_count = 1
@LimitationsManager.canAddXCollaborators(@project_id, 1, @callback)
it "should return true", ->
@@ -74,6 +87,7 @@ describe "LimitationsManager", ->
beforeEach ->
@current_number = 1
@allowed_number = 2
@invite_count = 0
@LimitationsManager.canAddXCollaborators(@project_id, 2, @callback)
it "should return false", ->
@@ -83,6 +97,7 @@ describe "LimitationsManager", ->
beforeEach ->
@current_number = 3
@allowed_number = 2
@invite_count = 0
@LimitationsManager.canAddXCollaborators(@project_id, 1, @callback)
it "should return false", ->
@@ -92,11 +107,31 @@ describe "LimitationsManager", ->
beforeEach ->
@current_number = 100
@allowed_number = -1
@invite_count = 0
@LimitationsManager.canAddXCollaborators(@project_id, 1, @callback)
it "should return true", ->
@callback.calledWith(null, true).should.equal true
describe 'when the project has more invites than allowed', ->
beforeEach ->
@current_number = 0
@allowed_number = 2
@invite_count = 2
@LimitationsManager.canAddXCollaborators(@project_id, 1, @callback)
it "should return false", ->
@callback.calledWith(null, false).should.equal true
describe 'when the project has more invites and collaborators than allowed', ->
beforeEach ->
@current_number = 1
@allowed_number = 2
@invite_count = 1
@LimitationsManager.canAddXCollaborators(@project_id, 1, @callback)
it "should return false", ->
@callback.calledWith(null, false).should.equal true
describe "userHasSubscription", ->
beforeEach ->
@@ -193,7 +228,7 @@ describe "LimitationsManager", ->
@LimitationsManager.userHasSubscriptionOrIsGroupMember @user, (err, hasSubOrIsGroupMember)->
hasSubOrIsGroupMember.should.equal false
done()
describe "hasGroupMembersLimitReached", ->
beforeEach ->
@@ -214,10 +249,10 @@ describe "LimitationsManager", ->
@LimitationsManager.hasGroupMembersLimitReached @user_id, (err, limitReached)->
limitReached.should.equal false
done()
it "should return true if the limit has been exceded", (done)->
@subscription.membersLimit = 0
@SubscriptionLocator.getUsersSubscription.callsArgWith(1, null, @subscription)
@LimitationsManager.hasGroupMembersLimitReached @user_id, (err, limitReached)->
limitReached.should.equal true
done()
done()

View File

@@ -728,7 +728,7 @@ describe "RecurlyWrapper", ->
describe 'when the account does not exist', ->
beforeEach ->
@apiRequest.callsArgWith(1, new Error('not found'), {statusCode: 404}, '')
@apiRequest.callsArgWith(1, null, {statusCode: 404}, '')
it 'should not produce an error', (done) ->
@call (err, result) =>

View File

@@ -0,0 +1,529 @@
expect = require("chai").expect
Async = require("async")
User = require "./helpers/User"
request = require "./helpers/request"
settings = require "settings-sharelatex"
CollaboratorsEmailHandler = require "../../../app/js/Features/Collaborators/CollaboratorsEmailHandler"
createInvite = (sendingUser, projectId, email, callback=(err, invite)->) ->
sendingUser.getCsrfToken (err) ->
return callback(err) if err
sendingUser.request.post {
uri: "/project/#{projectId}/invite",
json:
email: email
privileges: 'readAndWrite'
}, (err, response, body) ->
return callback(err) if err
callback(null, body.invite)
createProject = (owner, projectName, callback=(err, projectId, project)->) ->
owner.createProject projectName, (err, projectId) ->
throw err if err
fakeProject = {
_id: projectId,
name: projectName,
owner_ref: owner
}
callback(err, projectId, fakeProject)
createProjectAndInvite = (owner, projectName, email, callback=(err, project, invite)->) ->
createProject owner, projectName, (err, projectId, project) ->
return callback(err) if err
createInvite owner, projectId, email, (err, invite) ->
return callback(err) if err
link = CollaboratorsEmailHandler._buildInviteUrl(project, invite)
callback(null, project, invite, link)
revokeInvite = (sendingUser, projectId, inviteId, callback=(err)->) ->
sendingUser.getCsrfToken (err) ->
return callback(err) if err
sendingUser.request.delete {
uri: "/project/#{projectId}/invite/#{inviteId}",
}, (err, response, body) ->
return callback(err) if err
callback(null)
# Actions
tryFollowInviteLink = (user, link, callback=(err, response, body)->) ->
user.request.get {
uri: link
baseUrl: null
}, callback
tryAcceptInvite = (user, invite, callback=(err, response, body)->) ->
user.request.post {
uri: "/project/#{invite.projectId}/invite/#{invite._id}/accept"
json:
token: invite.token
}, callback
tryRegisterUser = (user, email, redir, callback=(err, response, body)->) ->
user.getCsrfToken (error) =>
return callback(error) if error?
user.request.post {
url: "/register"
json:
email: email
password: "some_weird_password"
redir: redir
}, callback
tryFollowLoginLink = (user, loginLink, callback=(err, response, body)->) ->
user.getCsrfToken (error) =>
return callback(error) if error?
user.request.get loginLink, callback
tryLoginUser = (user, redir, callback=(err, response, body)->) ->
user.getCsrfToken (error) =>
return callback(error) if error?
user.request.post {
url: "/login"
json:
email: user.email
password: user.password
redir: redir
}, callback
tryGetInviteList = (user, projectId, callback=(err, response, body)->) ->
user.getCsrfToken (error) =>
return callback(error) if error?
user.request.get {
url: "/project/#{projectId}/invites"
json: true
}, callback
tryJoinProject = (user, projectId, callback=(err, response, body)->) ->
user.getCsrfToken (error) =>
return callback(error) if error?
user.request.post {
url: "/project/#{projectId}/join"
qs: {user_id: user._id}
auth:
user: settings.apis.web.user
pass: settings.apis.web.pass
sendImmediately: true
json: true
jar: false
}, callback
# Expectations
expectProjectAccess = (user, projectId, callback=(err,result)->) ->
# should have access to project
user.openProject projectId, (err) =>
expect(err).to.be.oneOf [null, undefined]
callback()
expectNoProjectAccess = (user, projectId, callback=(err,result)->) ->
# should not have access to project page
user.openProject projectId, (err) =>
expect(err).to.be.instanceof Error
callback()
expectInvitePage = (user, link, callback=(err,result)->) ->
# view invite
tryFollowInviteLink user, link, (err, response, body) ->
expect(err).to.be.oneOf [null, undefined]
expect(response.statusCode).to.equal 200
expect(body).to.match new RegExp("<title>Project Invite - .*</title>")
callback()
expectInvalidInvitePage = (user, link, callback=(err,result)->) ->
# view invalid invite
tryFollowInviteLink user, link, (err, response, body) ->
expect(err).to.be.oneOf [null, undefined]
expect(response.statusCode).to.equal 200
expect(body).to.match new RegExp("<title>Invalid Invite - .*</title>")
callback()
expectInviteRedirectToRegister = (user, link, callback=(err,result)->) ->
# view invite, redirect to `/register`
tryFollowInviteLink user, link, (err, response, body) ->
expect(err).to.be.oneOf [null, undefined]
expect(response.statusCode).to.equal 302
expect(response.headers.location).to.match new RegExp("^/register\?.*redir=.*$")
# follow redirect to register page and extract the redirectUrl from form
user.request.get response.headers.location, (err, response, body) ->
redirectUrl = body.match(/input name="redir" type="hidden" value="([^"]*)"/m)?[1]
loginUrl = body.match(/href="([^"]*)">\s*Login here/m)?[1]
expect(redirectUrl).to.not.be.oneOf [null, undefined]
expect(loginUrl).to.not.be.oneOf [null, undefined]
callback(null, redirectUrl, loginUrl)
expectLoginPage = (user, loginLink, callback=(err, result)->) ->
tryFollowLoginLink user, loginLink, (err, response, body) ->
expect(err).to.be.oneOf [null, undefined]
expect(response.statusCode).to.equal 200
expect(body).to.match new RegExp("<title>Login - .*</title>")
redirectUrl = body.match(/input name="redir" type="hidden" value="([^"]*)"/m)?[1]
callback(null, redirectUrl)
expectLoginRedirectToInvite = (user, redir, link, callback=(err, result)->) ->
tryLoginUser user, redir, (err, response, body) ->
expect(err).to.be.oneOf [null, undefined]
expect(response.statusCode).to.equal 200
expect(link).to.match new RegExp("^.*#{body.redir}\?.*$")
callback(null, null)
expectRegistrationRedirectToInvite = (user, email, redir, link, callback=(err, result)->) ->
tryRegisterUser user, email, redir, (err, response, body) ->
expect(err).to.be.oneOf [null, undefined]
expect(response.statusCode).to.equal 200
expect(link).to.match new RegExp("^.*#{body.redir}\?.*$")
callback(null, null)
expectInviteRedirectToProject = (user, link, invite, callback=(err,result)->) ->
# view invite, redirect straight to project
tryFollowInviteLink user, link, (err, response, body) ->
expect(err).to.be.oneOf [null, undefined]
expect(response.statusCode).to.equal 302
expect(response.headers.location).to.equal "/project/#{invite.projectId}"
callback()
expectAcceptInviteAndRedirect = (user, invite, callback=(err,result)->) ->
# should accept the invite and redirect to project
tryAcceptInvite user, invite, (err, response, body) =>
expect(err).to.be.oneOf [null, undefined]
expect(response.statusCode).to.equal 302
expect(response.headers.location).to.equal "/project/#{invite.projectId}"
callback()
expectInviteListCount = (user, projectId, count, callback=(err)->) ->
tryGetInviteList user, projectId, (err, response, body) ->
expect(err).to.be.oneOf [null, undefined]
expect(response.statusCode).to.equal 200
expect(body).to.have.all.keys ['invites']
expect(body.invites.length).to.equal count
callback()
expectInvitesInJoinProjectCount = (user, projectId, count, callback=(err,result)->) ->
tryJoinProject user, projectId, (err, response, body) ->
expect(err).to.be.oneOf [null, undefined]
expect(response.statusCode).to.equal 200
expect(body.project).to.contain.keys ['invites']
expect(body.project.invites.length).to.equal count
callback()
describe "ProjectInviteTests", ->
before (done) ->
@sendingUser = new User()
@user = new User()
@site_admin = new User({email: "admin@example.com"})
@email = 'smoketestuser@example.com'
@projectName = 'sharing test'
Async.series [
(cb) => @user.login cb
(cb) => @user.logout cb
(cb) => @sendingUser.login cb
], done
describe 'creating invites', ->
beforeEach (done) ->
@projectName = "wat"
@projectId = null
@fakeProject = null
done()
afterEach ->
describe 'creating two invites', ->
beforeEach (done) ->
Async.series [
(cb) =>
createProject @sendingUser, @projectName, (err, projectId, project) =>
@projectId = projectId
@fakeProject = project
cb()
], done
afterEach (done) ->
Async.series [
(cb) => @sendingUser.deleteProject(@projectId, cb)
(cb) => @sendingUser.deleteProject(@projectId, cb)
], done
it 'should allow the project owner to create and remove invites', (done) ->
@invite = null
Async.series [
(cb) => expectProjectAccess @sendingUser, @projectId, cb
(cb) => expectInviteListCount @sendingUser, @projectId, 0, cb
# create invite, check invite list count
(cb) => createInvite @sendingUser, @projectId, @email, (err, invite) =>
return cb(err) if err
@invite = invite
cb()
(cb) => expectInviteListCount @sendingUser, @projectId, 1, cb
(cb) => revokeInvite @sendingUser, @projectId, @invite._id, cb
(cb) => expectInviteListCount @sendingUser, @projectId, 0, cb
# and a second time
(cb) => createInvite @sendingUser, @projectId, @email, (err, invite) =>
return cb(err) if err
@invite = invite
cb()
(cb) => expectInviteListCount @sendingUser, @projectId, 1, cb
# check the joinProject view
(cb) => expectInvitesInJoinProjectCount @sendingUser, @projectId, 1, cb
# revoke invite
(cb) => revokeInvite @sendingUser, @projectId, @invite._id, cb
(cb) => expectInviteListCount @sendingUser, @projectId, 0, cb
(cb) => expectInvitesInJoinProjectCount @sendingUser, @projectId, 0, cb
], done
it 'should allow the project owner to many invites at once', (done) ->
@inviteOne = null
@inviteTwo = null
Async.series [
(cb) => expectProjectAccess @sendingUser, @projectId, cb
(cb) => expectInviteListCount @sendingUser, @projectId, 0, cb
# create first invite
(cb) => createInvite @sendingUser, @projectId, @email, (err, invite) =>
return cb(err) if err
@inviteOne = invite
cb()
(cb) => expectInviteListCount @sendingUser, @projectId, 1, cb
# and a second
(cb) => createInvite @sendingUser, @projectId, @email, (err, invite) =>
return cb(err) if err
@inviteTwo = invite
cb()
# should have two
(cb) => expectInviteListCount @sendingUser, @projectId, 2, cb
(cb) => expectInvitesInJoinProjectCount @sendingUser, @projectId, 2, cb
# revoke first
(cb) => revokeInvite @sendingUser, @projectId, @inviteOne._id, cb
(cb) => expectInviteListCount @sendingUser, @projectId, 1, cb
# revoke second
(cb) => revokeInvite @sendingUser, @projectId, @inviteTwo._id, cb
(cb) => expectInviteListCount @sendingUser, @projectId, 0, cb
], done
describe 'clicking the invite link', ->
beforeEach (done) ->
@projectId = null
@fakeProject = null
done()
describe "user is logged in already", ->
beforeEach (done) ->
Async.series [
(cb) =>
createProjectAndInvite @sendingUser, @projectName, @email, (err, project, invite, link) =>
@projectId = project._id
@fakeProject = project
@invite = invite
@link = link
cb()
(cb) =>
@user.login (err) =>
if err
throw err
cb()
], done
afterEach (done) ->
Async.series [
(cb) => @sendingUser.deleteProject(@projectId, cb)
(cb) => @sendingUser.deleteProject(@projectId, cb)
(cb) => revokeInvite(@sendingUser, @projectId, @invite._id, cb)
], done
describe 'user is already a member of the project', ->
beforeEach (done) ->
Async.series [
(cb) => expectInvitePage @user, @link, cb
(cb) => expectAcceptInviteAndRedirect @user, @invite, cb
(cb) => expectProjectAccess @user, @invite.projectId, cb
], done
describe 'when user clicks on the invite a second time', ->
it 'should just redirect to the project page', (done) ->
Async.series [
(cb) => expectProjectAccess @user, @invite.projectId, cb
(cb) => expectInviteRedirectToProject @user, @link, @invite, cb
(cb) => expectProjectAccess @user, @invite.projectId, cb
], done
describe 'when the user recieves another invite to the same project', ->
it 'should redirect to the project page', (done) ->
Async.series [
(cb) =>
createInvite @sendingUser, @projectId, @email, (err, invite) =>
if err
throw err
@secondInvite = invite
@secondLink = CollaboratorsEmailHandler._buildInviteUrl(@fakeProject, invite)
cb()
(cb) => expectInviteRedirectToProject @user, @secondLink, @secondInvite, cb
(cb) => expectProjectAccess @user, @invite.projectId, cb
(cb) => revokeInvite @sendingUser, @projectId, @secondInvite._id, cb
], done
describe 'user is not a member of the project', ->
it 'should not grant access if the user does not accept the invite', (done) ->
Async.series(
[
(cb) => expectInvitePage @user, @link, cb
(cb) => expectNoProjectAccess @user, @invite.projectId, cb
], done
)
it 'should render the invalid-invite page if the token is invalid', (done) ->
Async.series(
[
(cb) =>
link = @link.replace(@invite.token, 'not_a_real_token')
expectInvalidInvitePage @user, link, cb
(cb) => expectNoProjectAccess @user, @invite.projectId, cb
(cb) => expectNoProjectAccess @user, @invite.projectId, cb
], done
)
it 'should allow the user to accept the invite and access the project', (done) ->
Async.series(
[
(cb) => expectInvitePage @user, @link, cb
(cb) => expectAcceptInviteAndRedirect @user, @invite, cb
(cb) => expectProjectAccess @user, @invite.projectId, cb
], done
)
describe 'user is not logged in initially', ->
before (done) ->
@user.logout done
beforeEach (done) ->
Async.series [
(cb) =>
createProjectAndInvite @sendingUser, @projectName, @email, (err, project, invite, link) =>
@projectId = project._id
@fakeProject = project
@invite = invite
@link = link
cb()
], done
afterEach (done) ->
Async.series [
(cb) => @sendingUser.deleteProject(@projectId, cb)
(cb) => @sendingUser.deleteProject(@projectId, cb)
(cb) => revokeInvite(@sendingUser, @projectId, @invite._id, cb)
], done
describe 'registration prompt workflow with valid token', ->
it 'should redirect to the register page', (done) ->
Async.series [
(cb) => expectInviteRedirectToRegister(@user, @link, cb)
], done
it 'should allow user to accept the invite if the user registers a new account', (done) ->
Async.series [
(cb) =>
expectInviteRedirectToRegister @user, @link, (err, redirectUrl) =>
@_redir = redirectUrl
cb()
(cb) => expectRegistrationRedirectToInvite @user, "some_email@example.com", @_redir, @link, cb
(cb) => expectInvitePage @user, @link, cb
(cb) => expectAcceptInviteAndRedirect @user, @invite, cb
(cb) => expectProjectAccess @user, @invite.projectId, cb
], done
describe 'registration prompt workflow with non-valid token', ->
before (done)->
@user.logout done
it 'should redirect to the register page', (done) ->
Async.series [
(cb) => expectInviteRedirectToRegister(@user, @link, cb)
(cb) => expectNoProjectAccess @user, @invite.projectId, cb
], done
it 'should display invalid-invite if the user registers a new account', (done) ->
badLink = @link.replace(@invite.token, 'not_a_real_token')
Async.series [
(cb) =>
expectInviteRedirectToRegister @user, badLink, (err, redirectUrl) =>
@_redir = redirectUrl
cb()
(cb) => expectRegistrationRedirectToInvite @user, "some_email@example.com", @_redir, badLink, cb
(cb) => expectInvalidInvitePage @user, badLink, cb
(cb) => expectNoProjectAccess @user, @invite.projectId, cb
], done
describe 'login workflow with valid token', ->
before (done)->
@user.logout done
it 'should redirect to the register page', (done) ->
Async.series [
(cb) => expectInviteRedirectToRegister(@user, @link, cb)
(cb) => expectNoProjectAccess @user, @invite.projectId, cb
], done
it 'should allow the user to login to view the invite', (done) ->
Async.series [
(cb) =>
expectInviteRedirectToRegister @user, @link, (err, redirectUrl, loginUrl) =>
@_redir = redirectUrl
@_loginLink = loginUrl
cb()
(cb) =>
expectLoginPage @user, @_loginLink, (err, redirectUrl) =>
expect(@_redir).to.equal redirectUrl
cb()
(cb) => expectLoginRedirectToInvite @user, @_redir, @link, cb
(cb) => expectInvitePage @user, @link, cb
(cb) => expectNoProjectAccess @user, @invite.projectId, cb
], done
it 'should allow user to accept the invite if the user registers a new account', (done) ->
Async.series [
(cb) => expectInvitePage @user, @link, cb
(cb) => expectAcceptInviteAndRedirect @user, @invite, cb
(cb) => expectProjectAccess @user, @invite.projectId, cb
], done
describe 'login workflow with non-valid token', ->
before (done)->
@user.logout done
it 'should redirect to the register page', (done) ->
Async.series [
(cb) => expectInviteRedirectToRegister(@user, @link, cb)
(cb) => expectNoProjectAccess @user, @invite.projectId, cb
], done
it 'should show the invalid-invite page once the user has logged in', (done) ->
badLink = @link.replace(@invite.token, 'not_a_real_token')
Async.series [
(cb) =>
expectInviteRedirectToRegister @user, badLink, (err, redirectUrl, loginUrl) =>
@_redir = redirectUrl
@_loginLink = loginUrl
cb()
(cb) =>
expectLoginPage @user, @_loginLink, (err, redirectUrl) =>
expect(@_redir).to.equal redirectUrl
cb()
(cb) => expectLoginRedirectToInvite @user, @_redir, badLink, cb
(cb) => expectInvalidInvitePage @user, badLink, cb
(cb) => expectNoProjectAccess @user, @invite.projectId, cb
], done

View File

@@ -13,7 +13,7 @@ class User
@request = request.defaults({
jar: @jar
})
login: (callback = (error) ->) ->
@getCsrfToken (error) =>
return callback(error) if error?
@@ -28,6 +28,8 @@ class User
return callback(error) if error?
@id = user?._id?.toString()
@_id = user?._id?.toString()
@first_name = user?.first_name
@referal_id = user?.referal_id
callback()
logout: (callback = (error) ->) ->
@@ -59,7 +61,24 @@ class User
if !body?.project_id?
console.error "SOMETHING WENT WRONG CREATING PROJECT", response.statusCode, response.headers["location"], body
callback(null, body.project_id)
deleteProject: (project_id, callback=(error)) ->
@request.delete {
url: "/project/#{project_id}"
}, (error, response, body) ->
return callback(error) if error?
callback(null)
openProject: (project_id, callback=(error)) ->
@request.get {
url: "/project/#{project_id}"
}, (error, response, body) ->
return callback(error) if error?
if response.statusCode != 200
err = new Error("Non-success response when opening project: #{response.statusCode}")
return callback(err)
callback(null)
addUserToProject: (project_id, email, privileges, callback = (error, user) ->) ->
@request.post {
url: "/project/#{project_id}/users",
@@ -67,7 +86,7 @@ class User
}, (error, response, body) ->
return callback(error) if error?
callback(null, body.user)
makePublic: (project_id, level, callback = (error) ->) ->
@request.post {
url: "/project/#{project_id}/settings/admin",