Merge pull request #983 from sharelatex/ta-user-ties

Add Group Managers Management Page
This commit is contained in:
Timothée Alby 2018-10-09 14:00:59 +01:00 committed by GitHub
commit 914546d7d6
22 changed files with 669 additions and 143 deletions

View file

@ -0,0 +1,9 @@
Institution = require('../../models/Institution').Institution
logger = require("logger-sharelatex")
ObjectId = require('mongoose').Types.ObjectId
module.exports = InstitutionLocator =
findManagedInstitution: (managerId, callback)->
logger.log managerId: managerId, "finding managed Institution"
Institution.findOne managerIds: managerId, callback

View file

@ -52,18 +52,6 @@ module.exports =
return res.sendStatus 500
res.send()
renderSubscriptionGroupAdminPage: (req, res, next)->
user_id = AuthenticationController.getLoggedInUserId(req)
getManagedSubscription user_id, (error, subscription)->
return next(error) if error?
if !subscription?.groupPlan
return res.redirect("/user/subscription")
SubscriptionGroupHandler.getPopulatedListOfMembers subscription._id, (err, users)->
res.render "subscriptions/group_admin",
title: 'group_admin'
users: users
subscription: subscription
exportGroupCsv: (req, res)->
user_id = AuthenticationController.getLoggedInUserId(req)
logger.log user_id: user_id, "exporting group csv"

View file

@ -11,6 +11,7 @@ TeamInvitesHandler = require("./TeamInvitesHandler")
EmailHandler = require("../Email/EmailHandler")
settings = require("settings-sharelatex")
NotificationsBuilder = require("../Notifications/NotificationsBuilder")
UserMembershipViewModel = require("../UserMembership/UserMembershipViewModel")
module.exports = SubscriptionGroupHandler =
@ -31,12 +32,12 @@ module.exports = SubscriptionGroupHandler =
logger.err err:err, "error adding user to group"
return callback(err)
NotificationsBuilder.groupPlan(user, {subscription_id:subscription._id}).read()
userViewModel = buildUserViewModel(user)
userViewModel = UserMembershipViewModel.build(user)
callback(err, userViewModel)
else
TeamInvitesHandler.createInvite subscriptionId, newEmail, (err) ->
return callback(err) if err?
userViewModel = buildEmailInviteViewModel(newEmail)
userViewModel = UserMembershipViewModel.build(newEmail)
callback(err, userViewModel)
removeUserFromGroup: (subscriptionId, userToRemove_id, callback)->
@ -51,28 +52,6 @@ module.exports = SubscriptionGroupHandler =
replaceInArray Subscription, "member_ids", oldId, newId, callback
getPopulatedListOfMembers: (subscriptionId, callback)->
SubscriptionLocator.getSubscription subscriptionId, (err, subscription)->
users = []
for email in subscription.invited_emails or []
users.push buildEmailInviteViewModel(email)
for teamInvite in subscription.teamInvites or []
users.push buildEmailInviteViewModel(teamInvite.email)
jobs = _.map subscription.member_ids, (user_id)->
return (cb)->
UserGetter.getUser user_id, (err, user)->
if err? or !user?
users.push _id:user_id
return cb()
userViewModel = buildUserViewModel(user)
users.push(userViewModel)
cb()
async.series jobs, (err)->
callback(err, users)
isUserPartOfGroup: (user_id, subscription_id, callback=(err, partOfGroup)->)->
SubscriptionLocator.getSubscriptionByMemberIdAndId user_id, subscription_id, (err, subscription)->
if subscription?
@ -99,18 +78,3 @@ replaceInArray = (model, property, oldValue, newValue, callback) ->
model.update query, { $addToSet: setNewValue }, { multi: true }, (error) ->
return callback(error) if error?
model.update query, { $pull: setOldValue }, { multi: true }, callback
buildUserViewModel = (user)->
u =
email: user.email
first_name: user.first_name
last_name: user.last_name
invite: user.holdingAccount
_id: user._id
return u
buildEmailInviteViewModel = (email) ->
return {
email: email
invite: true
}

View file

@ -20,7 +20,8 @@ module.exports =
webRouter.get '/user/subscription/thank-you', AuthenticationController.requireLogin(), SubscriptionController.successful_subscription
webRouter.get '/subscription/group', AuthenticationController.requireLogin(), SubscriptionGroupController.renderSubscriptionGroupAdminPage
webRouter.get '/subscription/group', AuthenticationController.requireLogin(), (req, res, next) ->
res.redirect('/manage/group/members') # legacy route
webRouter.post '/subscription/group/user', AuthenticationController.requireLogin(), SubscriptionGroupController.addUserToGroup
webRouter.get '/subscription/group/export', AuthenticationController.requireLogin(), SubscriptionGroupController.exportGroupCsv
webRouter.delete '/subscription/group/user/:user_id', AuthenticationController.requireLogin(), SubscriptionGroupController.removeUserFromGroup

View file

@ -55,7 +55,10 @@ module.exports = SubscriptionUpdater =
if err?
logger.err err:err, searchOps:searchOps, removeOperation:removeOperation, "error removing user from group"
return callback(err)
FeaturesUpdater.refreshFeatures user_id, callback
UserGetter.getUserOrUserStubById user_id, {}, (error, user, isStub) ->
return callback(error) if error
return callback() if isStub
FeaturesUpdater.refreshFeatures user_id, callback
deleteWithV1Id: (v1TeamId, callback)->
Subscription.deleteOne { "overleaf.id": v1TeamId }, callback

View file

@ -66,15 +66,18 @@ module.exports = UserGetter =
db.users.find { _id: { $in: user_ids} }, projection, callback
getUserOrUserStubById: (user_id, projection, callback = (error, user) ->) ->
getUserOrUserStubById: (user_id, projection, callback = (error, user, isStub) ->) ->
try
query = _id: ObjectId(user_id.toString())
catch e
return callback(new Error(e))
db.users.findOne query, projection, (error, user) ->
return callback(error) if error?
return callback(null, user) if user?
db.userstubs.findOne query, projection, callback
return callback(null, user, false) if user?
db.userstubs.findOne query, projection, (error, user) ->
return callback(error) if error
return callback() if !user?
callback(null, user, true)
# check for duplicate email address. This is also enforced at the DB level
ensureUniqueEmailAddress: (newEmail, callback) ->

View file

@ -0,0 +1,65 @@
AuthenticationController = require('../Authentication/AuthenticationController')
UserMembershipHandler = require('./UserMembershipHandler')
logger = require("logger-sharelatex")
module.exports =
index: (entityName, req, res, next)->
userId = AuthenticationController.getLoggedInUserId(req)
UserMembershipHandler.getEntity entityName, userId, (error, entity)->
return next(error) if error?
UserMembershipHandler.getUsers entityName, entity, (error, users)->
return next(error) if error?
res.render "user_membership/index",
users: users
entity: entity
translations: getTranslationsFor(entityName)
paths: getPathsFor(entityName)
add: (entityName, req, res, next)->
userId = AuthenticationController.getLoggedInUserId(req)
email = req.body.email
return res.sendStatus 422 unless email
UserMembershipHandler.getEntity entityName, userId, (error, entity)->
return next(error) if error?
UserMembershipHandler.addUser entityName, entity, email, (error, user)->
return next(error) if error?
res.json(user: user)
remove: (entityName, req, res, next)->
loggedInUserId = AuthenticationController.getLoggedInUserId(req)
userId = req.params.userId
UserMembershipHandler.getEntity entityName, loggedInUserId, (error, entity)->
return next(error) if error?
UserMembershipHandler.removeUser entityName, entity, userId, (error, user)->
return next(error) if error?
res.send()
getTranslationsFor = (entityName) ->
switch entityName
when 'group'
title: 'group_account'
remove: 'remove_from_group'
when 'groupManagers'
title: 'group_managers'
remove: 'remove_manager'
when 'institution'
title: 'institution_managers'
remove: 'remove_manager'
getPathsFor = (entityName) ->
switch entityName
when 'group'
addMember: '/subscription/invites'
removeMember: '/subscription/group/user'
removeInvite: '/subscription/invites'
exportMembers: '/subscription/group/export'
when 'groupManagers'
addMember: "/manage/group/managers"
removeMember: "/manage/group/managers"
when 'institution'
addMember: "/manage/institution/managers"
removeMember: "/manage/institution/managers"

View file

@ -0,0 +1,85 @@
async = require("async")
Errors = require('../Errors/Errors')
SubscriptionLocator = require('../Subscription/SubscriptionLocator')
InstitutionsLocator = require('../Institutions/InstitutionsLocator')
UserMembershipViewModel = require('./UserMembershipViewModel')
UserGetter = require('../User/UserGetter')
logger = require('logger-sharelatex')
module.exports =
getEntity: (entityName, userId, callback = (error, entity) ->) ->
switch entityName
when 'group' then getGroupSubscription(userId, callback)
when 'groupManagers'
getGroupSubscription userId, (error, subscription) ->
subscription.membersLimit = null if subscription # managers are unlimited
callback(error, subscription)
when 'institution' then getInstitution(userId, callback)
else callback(new Errors.NotFoundError("No such entity: #{entityName}"))
getUsers: (entityName, entity, callback = (error, users) ->) ->
attributes = switch entityName
when 'group' then ['invited_emails', 'teamInvites', 'member_ids']
when 'groupManagers' then ['manager_ids']
when 'institution' then ['managerIds']
getPopulatedListOfMembers(entity, attributes, callback)
addUser: (entityName, entity, email, callback = (error, user) ->) ->
attribute = switch entityName
when 'groupManagers' then 'manager_ids'
when 'institution' then 'managerIds'
unless attribute
return callback(new Errors.NotFoundError("Cannot add user to entity: #{entityName}"))
UserGetter.getUserByAnyEmail email, (error, user) ->
error ||= new Errors.NotFoundError("No user found with email #{email}") unless user
return callback(error) if error?
addUserToEntity entity, attribute, user, (error) ->
callback(error, UserMembershipViewModel.build(user))
removeUser: (entityName, entity, userId, callback = (error) ->) ->
attribute = switch entityName
when 'groupManagers' then 'manager_ids'
when 'institution' then 'managerIds'
else callback(new Errors.NotFoundError("Cannot remove user from entity: #{entityName}"))
removeUserFromEntity entity, attribute, userId, callback
getGroupSubscription = (managerId, callback = (error, subscription) ->) ->
SubscriptionLocator.findManagedSubscription managerId, (err, subscription)->
if subscription? and subscription.groupPlan
logger.log managerId: managerId, 'got managed subscription'
else
err ||= new Errors.NotFoundError("No subscription found managed by user #{managerId}")
callback(err, subscription)
getInstitution = (managerId, callback = (error, institution) ->) ->
InstitutionsLocator.findManagedInstitution managerId, (err, institution)->
if institution?
logger.log managerId: managerId, 'got managed subscription'
else
err ||= new Errors.NotFoundError("No institution found managed by user #{managerId}")
callback(err, institution)
getPopulatedListOfMembers = (entity, attributes, callback = (error, users)->)->
userObjects = []
for attribute in attributes
for userObject in entity[attribute] or []
# userObject can be an email as String, a user id as ObjectId or an
# invite as Object with an email attribute as String. We want to pass to
# UserMembershipViewModel either an email as (String) or a user id (ObjectId)
userIdOrEmail = userObject.email || userObject
userObjects.push userIdOrEmail
async.map userObjects, UserMembershipViewModel.buildAsync, callback
addUserToEntity = (entity, attribute, user, callback = (error)->) ->
fieldUpdate = {}
fieldUpdate[attribute] = user._id
entity.update { $addToSet: fieldUpdate }, callback
removeUserFromEntity = (entity, attribute, userId, callback = (error)->) ->
fieldUpdate = {}
fieldUpdate[attribute] = userId
entity.update { $pull: fieldUpdate }, callback

View file

@ -0,0 +1,26 @@
AuthenticationController = require('../Authentication/AuthenticationController')
UserMembershipController = require './UserMembershipController'
module.exports =
apply: (webRouter) ->
webRouter.get '/manage/group/members',
AuthenticationController.requireLogin(),
(req, res, next) -> UserMembershipController.index('group', req, res, next)
regularEntitites =
group: 'groupManagers'
institution: 'institution'
for pathName, entityName of regularEntitites
do (pathName, entityName) ->
webRouter.get "/manage/#{pathName}/managers",
AuthenticationController.requireLogin(),
(req, res, next) -> UserMembershipController.index(entityName, req, res, next)
webRouter.post "/manage/#{pathName}/managers",
AuthenticationController.requireLogin(),
(req, res, next) -> UserMembershipController.add(entityName, req, res, next)
webRouter.delete "/manage/#{pathName}/managers/:userId",
AuthenticationController.requireLogin(),
(req, res, next) -> UserMembershipController.remove(entityName, req, res, next)

View file

@ -0,0 +1,45 @@
ObjectId = require('mongojs').ObjectId
UserGetter = require('../User/UserGetter')
module.exports = UserMembershipViewModel =
build: (userOrEmail) ->
if userOrEmail._id
buildUserViewModel userOrEmail
else
buildUserViewModelWithEmail userOrEmail
buildAsync: (userOrIdOrEmail, callback = (error, viewModel)->) ->
unless userOrIdOrEmail instanceof ObjectId
# userOrIdOrEmail is a user or an email and can be parsed by #build
return callback(null, UserMembershipViewModel.build(userOrIdOrEmail))
userId = userOrIdOrEmail
projection = { email: 1, first_name: 1, last_name: 1 }
UserGetter.getUserOrUserStubById userId, projection, (error, user, isStub) ->
if error? or !user?
return callback(null, buildUserViewModelWithId(userId.toString()))
if isStub
return callback(null, buildUserViewModelWithStub(user))
callback(null, buildUserViewModel(user))
buildUserViewModel = (user, isInvite = false) ->
_id: user._id or null
email: user.email or null
first_name: user.first_name or null
last_name: user.last_name or null
invite: isInvite
buildUserViewModelWithEmail = (email) ->
buildUserViewModel({ email }, true)
buildUserViewModelWithStub = (user) ->
# user stubs behave as invites
buildUserViewModel(user, true)
buildUserViewModelWithId = (id) ->
buildUserViewModel({ _id: id }, false)

View file

@ -0,0 +1,11 @@
mongoose = require 'mongoose'
Schema = mongoose.Schema
ObjectId = Schema.ObjectId
InstitutionSchema = new Schema
v1Id: { type: Number, required: true }
managerIds: [ type:ObjectId, ref:'User' ]
mongoose.model 'Institution', InstitutionSchema
exports.Institution = mongoose.model 'Institution'
exports.InstitutionSchema = InstitutionSchema

View file

@ -50,6 +50,7 @@ TokenAccessController = require('./Features/TokenAccess/TokenAccessController')
Features = require('./infrastructure/Features')
LinkedFilesRouter = require './Features/LinkedFiles/LinkedFilesRouter'
TemplatesRouter = require './Features/Templates/TemplatesRouter'
UserMembershipRouter = require './Features/UserMembership/UserMembershipRouter'
logger = require("logger-sharelatex")
_ = require("underscore")
@ -84,6 +85,7 @@ module.exports = class Router
AnalyticsRouter.apply(webRouter, privateApiRouter, publicApiRouter)
LinkedFilesRouter.apply(webRouter, privateApiRouter, publicApiRouter)
TemplatesRouter.apply(webRouter)
UserMembershipRouter.apply(webRouter)
Modules.applyRouter(webRouter, privateApiRouter, publicApiRouter)

View file

@ -5,16 +5,16 @@ block content
.container
.row
.col-md-10.col-md-offset-1
.card(ng-controller="SubscriptionGroupMembersController")
.card(ng-controller="UserMembershipController")
.page-header
.pull-right(ng-cloak)
small(ng-show="selectedUsers.length == 0") !{translate("you_have_added_x_of_group_size_y", {addedUsersSize:"<strong>{{ users.length }}</strong>", groupSize:"<strong>{{ groupSize }}</strong>"})}
small(ng-show="groupSize && selectedUsers.length == 0") !{translate("you_have_added_x_of_group_size_y", {addedUsersSize:"<strong>{{ users.length }}</strong>", groupSize:"<strong>{{ groupSize }}</strong>"})}
a.btn.btn-danger(
href,
ng-show="selectedUsers.length > 0"
ng-click="removeMembers()"
) #{translate("remove_from_group")}
h1 #{translate("group_account")}
) #{translate(translations.remove)}
h1 #{translate(translations.title)}
.row-spaced-small
ul.list-unstyled.structured-list(
@ -35,7 +35,7 @@ block content
span.header #{translate("accepted_invite")}
li.container-fluid(
ng-repeat="user in users | orderBy:'email':true",
ng-controller="SubscriptionGroupMemberListItemController"
ng-controller="UserMembershipListItemController"
)
.row
.col-md-5
@ -60,7 +60,7 @@ block content
small #{translate("no_members")}
hr
div(ng-if="users.length < groupSize", ng-cloak)
div(ng-if="!groupSize || users.length < groupSize", ng-cloak)
p.small #{translate("add_more_members")}
form.form
.row
@ -74,18 +74,16 @@ block content
)
.col-xs-4
button.btn.btn-primary(ng-click="addMembers()") #{translate("add")}
.col-xs-2
a(href="/subscription/group/export") Export CSV
.col-xs-2(ng-if="paths.exportMembers", ng-cloak)
a(href=paths.exportMembers) Export CSV
div(ng-if="users.length >= groupSize && users.length > 0", ng-cloak)
div(ng-if="groupSize && users.length >= groupSize && users.length > 0", ng-cloak)
.row
.col-xs-2.col-xs-offset-10
a(href="/subscription/group/export") Export CSV
.col-xs-2.col-xs-offset-10(ng-if="paths.exportMembers", ng-cloak)
a(href=paths.exportMembers) Export CSV
script(type="text/javascript").
window.users = !{JSON.stringify(users)};
window.groupSize = #{subscription.membersLimit};
window.paths = !{JSON.stringify(paths)};
window.groupSize = #{entity.membersLimit || 'null'};

View file

@ -5,7 +5,7 @@ define [
"main/clear-sessions"
"main/account-upgrade"
"main/plans"
"main/group-members"
"main/user-membership"
"main/scribtex-popup"
"main/event"
"main/bonus"

View file

@ -1,9 +1,10 @@
define [
"base"
], (App) ->
App.controller "SubscriptionGroupMembersController", ($scope, queuedHttp) ->
App.controller "UserMembershipController", ($scope, queuedHttp) ->
$scope.users = window.users
$scope.groupSize = window.groupSize
$scope.paths = window.paths
$scope.selectedUsers = []
$scope.inputs =
@ -22,7 +23,7 @@ define [
emails = parseEmails($scope.inputs.emails)
for email in emails
queuedHttp
.post("/subscription/invites", {
.post(paths.addMember, {
email: email,
_csrf: window.csrfToken
})
@ -34,10 +35,12 @@ define [
$scope.removeMembers = () ->
for user in $scope.selectedUsers
do (user) ->
if user.invite and !user._id?
url = "/subscription/invites/#{encodeURIComponent(user.email)}"
if paths.removeInvite and user.invite and !user._id?
url = "#{paths.removeInvite}/#{encodeURIComponent(user.email)}"
else if paths.removeMember and user._id?
url = "#{paths.removeMember}/#{user._id}"
else
url = "/subscription/group/user/#{user._id}"
return
queuedHttp({
method: "DELETE",
url: url
@ -53,7 +56,7 @@ define [
$scope.updateSelectedUsers = () ->
$scope.selectedUsers = $scope.users.filter (user) -> user.selected
App.controller "SubscriptionGroupMemberListItemController", ($scope) ->
App.controller "UserMembershipListItemController", ($scope) ->
$scope.$watch "user.selected", (value) ->
if value?
$scope.updateSelectedUsers()

View file

@ -0,0 +1,29 @@
SandboxedModule = require('sandboxed-module')
should = require('chai').should()
sinon = require('sinon')
assertCalledWith = sinon.assert.calledWith
assertNotCalled = sinon.assert.notCalled
modulePath = "../../../../app/js/Features/Institutions/InstitutionsLocator"
assert = require("chai").assert
ObjectId = require('mongoose').Types.ObjectId
describe 'InstitutionsLocator', ->
beforeEach ->
@user =
_id: "5208dd34438842e2db333333"
@institution =
v1Id: 123
managersIds: [ObjectId(), ObjectId()]
@Institution =
findOne: sinon.stub().yields(null, @institution)
@InstitutionsLocator = SandboxedModule.require modulePath, requires:
'../../models/Institution': Institution: @Institution
"logger-sharelatex": log:->
describe "finding managed institution", ->
it "should query the database", (done) ->
@InstitutionsLocator.findManagedInstitution @user._id, (err, institution)=>
assertCalledWith(@Institution.findOne, { managerIds: @user._id })
institution.should.equal @institution
done()

View file

@ -81,25 +81,6 @@ describe "SubscriptionGroupController", ->
done()
@Controller.removeUserFromGroup @req, res
describe "renderSubscriptionGroupAdminPage", ->
it "should redirect you if you don't have a group account", (done)->
@subscription.groupPlan = false
res =
redirect : (path)=>
path.should.equal("/user/subscription")
done()
@Controller.renderSubscriptionGroupAdminPage @req, res
it "should redirect you don't have a subscription", (done)->
@SubscriptionLocator.getUsersSubscription = sinon.stub().callsArgWith(1)
res =
redirect : (path)=>
path.should.equal("/user/subscription")
done()
@Controller.renderSubscriptionGroupAdminPage @req, res
describe "exportGroupCsv", ->
beforeEach ->

View file

@ -157,52 +157,6 @@ describe "SubscriptionGroupHandler", ->
{ $pull: { member_ids: @oldId } }
).should.equal true
describe "getPopulatedListOfMembers", ->
beforeEach ->
@subscription = {}
@SubscriptionLocator.getSubscription.callsArgWith(1, null, @subscription)
@UserGetter.getUser.callsArgWith(1, null, {_id:"31232"})
it "should locate the subscription", (done)->
@UserGetter.getUser.callsArgWith(1, null, {_id:"31232"})
@Handler.getPopulatedListOfMembers @subscriptionId, (err, users)=>
@SubscriptionLocator.getSubscription.calledWith(@subscriptionId).should.equal true
done()
it "should get the users by id", (done)->
@UserGetter.getUser.callsArgWith(1, null, {_id:"31232"})
@subscription.member_ids = ["1234", "342432", "312312"]
@Handler.getPopulatedListOfMembers @adminUser_id, (err, users)=>
@UserGetter.getUser.calledWith(@subscription.member_ids[0]).should.equal true
@UserGetter.getUser.calledWith(@subscription.member_ids[1]).should.equal true
@UserGetter.getUser.calledWith(@subscription.member_ids[2]).should.equal true
users.length.should.equal @subscription.member_ids.length
done()
it "should just return the id if the user can not be found as they may have deleted their account", (done)->
@UserGetter.getUser.callsArgWith(1)
@subscription.member_ids = ["1234", "342432", "312312"]
@Handler.getPopulatedListOfMembers @adminUser_id, (err, users)=>
assert.deepEqual users[0], {_id:@subscription.member_ids[0]}
assert.deepEqual users[1], {_id:@subscription.member_ids[1]}
assert.deepEqual users[2], {_id:@subscription.member_ids[2]}
done()
it "should return any invited users", (done) ->
@subscription.invited_emails = [ "jo@example.com" ]
@subscription.teamInvites = [
{ email: "charlie@example.com" }
]
@Handler.getPopulatedListOfMembers @adminUser_id, (err, users)=>
users[0].email.should.equal "jo@example.com"
users[0].invite.should.equal true
users[1].email.should.equal "charlie@example.com"
users[1].invite.should.equal true
users.length.should.equal @subscription.teamInvites.length + @subscription.invited_emails.length
done()
describe "isUserPartOfGroup", ->
beforeEach ->
@subscription_id = "123ed13123"

View file

@ -17,6 +17,7 @@ describe "SubscriptionUpdater", ->
_id: @adminuser_id = "5208dd34438843e2db000007"
@otherUserId = "5208dd34438842e2db000005"
@allUserIds = ["13213", "dsadas", "djsaiud89"]
@userStub = _id: 'mock-user-stub-id', email: 'mock-stub-email@baz.com'
@subscription = subscription =
_id: "111111111111111111111111"
admin_id: @adminUser._id
@ -67,6 +68,7 @@ describe "SubscriptionUpdater", ->
getUsers: (memberIds, projection, callback) ->
users = memberIds.map (id) -> { _id: id }
callback(null, users)
getUserOrUserStubById: sinon.stub()
@ReferalFeatures = getBonusFeatures: sinon.stub().callsArgWith(1)
@Modules = {hooks: {fire: sinon.stub().callsArgWith(2, null, null)}}
@ -190,6 +192,7 @@ describe "SubscriptionUpdater", ->
describe "removeUserFromGroup", ->
beforeEach ->
@FeaturesUpdater.refreshFeatures = sinon.stub().callsArgWith(1)
@UserGetter.getUserOrUserStubById.yields(null, {}, false)
it "should pull the users id from the group", (done)->
@SubscriptionUpdater.removeUserFromGroup @subscription._id, @otherUserId, =>
@ -205,6 +208,12 @@ describe "SubscriptionUpdater", ->
@FeaturesUpdater.refreshFeatures.calledWith(@otherUserId).should.equal true
done()
it "should not update features for user stubs", (done)->
@UserGetter.getUserOrUserStubById.yields(null, {}, true)
@SubscriptionUpdater.removeUserFromGroup @subscription._id, @userStub._id, =>
@FeaturesUpdater.refreshFeatures.called.should.equal false
done()
describe "deleteSubscription", ->
beforeEach (done) ->
@subscription_id = ObjectId().toString()

View file

@ -0,0 +1,95 @@
sinon = require('sinon')
assertCalledWith = sinon.assert.calledWith
assertNotCalled = sinon.assert.notCalled
chai = require('chai')
should = chai.should()
assert = chai.assert
expect = require('chai').expect
modulePath = "../../../../app/js/Features/UserMembership/UserMembershipController.js"
SandboxedModule = require('sandboxed-module')
MockRequest = require "../helpers/MockRequest"
MockResponse = require "../helpers/MockResponse"
describe "UserMembershipController", ->
beforeEach ->
@req = new MockRequest()
@user = _id: 'mock-user-id'
@newUser = _id: 'mock-new-user-id', email: 'new-user-email@foo.bar'
@subscription = { _id: 'mock-subscription-id'}
@users = [{ _id: 'mock-member-id-1' }, { _id: 'mock-member-id-2' }]
@AuthenticationController =
getLoggedInUserId: sinon.stub().returns(@user._id)
@UserMembershipHandler =
getEntity: sinon.stub().yields(null, @subscription)
getUsers: sinon.stub().yields(null, @users)
addUser: sinon.stub().yields(null, @newUser)
removeUser: sinon.stub().yields(null)
@UserMembershipController = SandboxedModule.require modulePath, requires:
'../Authentication/AuthenticationController': @AuthenticationController
'./UserMembershipHandler': @UserMembershipHandler
"logger-sharelatex":
log: ->
err: ->
describe 'index', ->
it 'get entity', (done) ->
@UserMembershipController.index 'group', @req, render: () =>
sinon.assert.calledWith(@UserMembershipHandler.getEntity, 'group', @user._id)
done()
it 'get users', (done) ->
@UserMembershipController.index 'group', @req, render: () =>
sinon.assert.calledWith(@UserMembershipHandler.getUsers, 'group', @subscription)
done()
it 'render group view', (done) ->
@UserMembershipController.index 'group', @req, render: (viewPath, viewParams) =>
expect(viewPath).to.equal 'user_membership/index'
expect(viewParams.entity).to.deep.equal @subscription
expect(viewParams.users).to.deep.equal @users
expect(viewParams.translations.title).to.equal 'group_account'
expect(viewParams.paths.addMember).to.equal '/subscription/invites'
done()
it 'render group managers view', (done) ->
@UserMembershipController.index 'groupManagers', @req, render: (viewPath, viewParams) =>
expect(viewPath).to.equal 'user_membership/index'
expect(viewParams.translations.title).to.equal 'group_managers'
expect(viewParams.paths.exportMembers).to.be.undefined
done()
it 'render institution view', (done) ->
@UserMembershipController.index 'institution', @req, render: (viewPath, viewParams) =>
expect(viewPath).to.equal 'user_membership/index'
expect(viewParams.translations.title).to.equal 'institution_managers'
expect(viewParams.paths.exportMembers).to.be.undefined
done()
describe 'add', ->
beforeEach ->
@req.body.email = @newUser.email
it 'get entity', (done) ->
@UserMembershipController.add 'groupManagers', @req, json: () =>
sinon.assert.calledWith(@UserMembershipHandler.getEntity, 'groupManagers', @user._id)
done()
it 'add user', (done) ->
@UserMembershipController.add 'groupManagers', @req, json: () =>
sinon.assert.calledWith(@UserMembershipHandler.addUser, 'groupManagers', @subscription, @newUser.email)
done()
it 'return user object', (done) ->
@UserMembershipController.add 'groupManagers', @req, json: (payload) =>
payload.user.should.equal @newUser
done()
describe 'remove', ->
beforeEach ->
@req.params.userId = @newUser._id
it 'remove user', (done) ->
@UserMembershipController.remove 'groupManagers', @req, send: () =>
sinon.assert.calledWith(@UserMembershipHandler.removeUser, 'groupManagers', @subscription, @newUser._id)
done()

View file

@ -0,0 +1,172 @@
chai = require('chai')
should = chai.should()
expect = require('chai').expect
sinon = require('sinon')
assertCalledWith = sinon.assert.calledWith
assertNotCalled = sinon.assert.notCalled
ObjectId = require("../../../../app/js/infrastructure/mongojs").ObjectId
modulePath = "../../../../app/js/Features/UserMembership/UserMembershipHandler"
SandboxedModule = require("sandboxed-module")
Errors = require("../../../../app/js/Features/Errors/Errors")
describe 'UserMembershipHandler', ->
beforeEach ->
@user = _id: 'mock-user-id'
@newUser = _id: 'mock-new-user-id', email: 'new-user-email@foo.bar'
@subscription =
_id: 'mock-subscription-id'
groupPlan: true
membersLimit: 10
member_ids: [ObjectId(), ObjectId()]
manager_ids: [ObjectId()]
invited_emails: ['mock-email-1@foo.com']
teamInvites: [{ email: 'mock-email-1@bar.com' }]
update: sinon.stub().yields(null)
@institution =
_id: 'mock-institution-id'
v1Id: 123
managerIds: [ObjectId(), ObjectId(), ObjectId()]
update: sinon.stub().yields(null)
@SubscriptionLocator =
findManagedSubscription: sinon.stub().yields(null, @subscription)
@InstitutionsLocator =
findManagedInstitution: sinon.stub().yields(null, @institution)
@UserMembershipViewModel =
buildAsync: sinon.stub().yields(null, { _id: 'mock-member-id'})
build: sinon.stub().returns(@newUser)
@UserGetter =
getUserByAnyEmail: sinon.stub().yields(null, @newUser)
@UserMembershipHandler = SandboxedModule.require modulePath, requires:
'../Subscription/SubscriptionLocator': @SubscriptionLocator
'../Institutions/InstitutionsLocator': @InstitutionsLocator
'./UserMembershipViewModel': @UserMembershipViewModel
'../User/UserGetter': @UserGetter
'../Errors/Errors': Errors
'logger-sharelatex':
log: ->
err: ->
describe 'getEntty', ->
it 'validate type', (done) ->
@UserMembershipHandler.getEntity 'foo', null, (error) ->
should.exist(error)
expect(error.message).to.match /No such entity/
done()
describe 'group subscriptions', ->
it 'get subscription', (done) ->
@UserMembershipHandler.getEntity 'group', @user._id, (error, subscription) =>
should.not.exist(error)
assertCalledWith(@SubscriptionLocator.findManagedSubscription, @user._id)
expect(subscription).to.equal @subscription
expect(subscription.membersLimit).to.equal 10
done()
it 'check subscription is a group', (done) ->
@SubscriptionLocator.findManagedSubscription.yields(null, { groupPlan: false })
@UserMembershipHandler.getEntity 'group', @user._id, (error, subscription) ->
should.exist(error)
done()
it 'handle error', (done) ->
@SubscriptionLocator.findManagedSubscription.yields(new Error('some error'))
@UserMembershipHandler.getEntity 'group', @user._id, (error, subscription) =>
should.exist(error)
done()
describe 'group managers', ->
it 'has no members limit', (done) ->
@UserMembershipHandler.getEntity 'groupManagers', @user._id, (error, subscription) =>
should.not.exist(error)
assertCalledWith(@SubscriptionLocator.findManagedSubscription, @user._id)
expect(subscription.membersLimit).to.equal null
done()
describe 'institutions', ->
it 'get institution', (done) ->
@UserMembershipHandler.getEntity 'institution', @user._id, (error, institution) =>
should.not.exist(error)
assertCalledWith(@InstitutionsLocator.findManagedInstitution, @user._id)
expect(institution).to.equal @institution
done()
it 'handle institution not found', (done) ->
@InstitutionsLocator.findManagedInstitution.yields(null, null)
@UserMembershipHandler.getEntity 'institution', @user._id, (error, institution) =>
should.exist(error)
expect(error).to.be.an.instanceof(Errors.NotFoundError)
done()
it 'handle errors', (done) ->
@InstitutionsLocator.findManagedInstitution.yields(new Error('nope'))
@UserMembershipHandler.getEntity 'institution', @user._id, (error, institution) =>
should.exist(error)
expect(error).to.not.be.an.instanceof(Errors.NotFoundError)
done()
describe 'getUsers', ->
describe 'group', ->
it 'build view model for all users', (done) ->
@UserMembershipHandler.getUsers 'group', @subscription, (error, users) =>
expectedCallcount =
@subscription.member_ids.length +
@subscription.invited_emails.length +
@subscription.teamInvites.length
expect(@UserMembershipViewModel.buildAsync.callCount).to.equal expectedCallcount
done()
describe 'group mamagers', ->
it 'build view model for all managers', (done) ->
@UserMembershipHandler.getUsers 'groupManagers', @subscription, (error, users) =>
expectedCallcount = @subscription.manager_ids.length
expect(@UserMembershipViewModel.buildAsync.callCount).to.equal expectedCallcount
done()
describe 'institution', ->
it 'build view model for all managers', (done) ->
@UserMembershipHandler.getUsers 'institution', @institution, (error, users) =>
expectedCallcount = @institution.managerIds.length
expect(@UserMembershipViewModel.buildAsync.callCount).to.equal expectedCallcount
done()
describe 'addUser', ->
beforeEach ->
@email = @newUser.email
describe 'group', ->
it 'fails', (done) ->
@UserMembershipHandler.addUser 'group', @subscription, @email, (error) =>
expect(error).to.exist
done()
describe 'institution', ->
it 'get user', (done) ->
@UserMembershipHandler.addUser 'institution', @institution, @email, (error, user) =>
assertCalledWith(@UserGetter.getUserByAnyEmail, @email)
done()
it 'handle user not found', (done) ->
@UserGetter.getUserByAnyEmail.yields(null, null)
@UserMembershipHandler.addUser 'institution', @institution, @email, (error) =>
expect(error).to.exist
expect(error).to.be.an.instanceof(Errors.NotFoundError)
done()
it 'add user to institution', (done) ->
@UserMembershipHandler.addUser 'institution', @institution, @email, (error, user) =>
assertCalledWith(@institution.update, { $addToSet: managerIds: @newUser._id })
done()
it 'return user view', (done) ->
@UserMembershipHandler.addUser 'institution', @institution, @email, (error, user) =>
user.should.equal @newUser
done()
describe 'removeUser', ->
describe 'institution', ->
it 'remove user from institution', (done) ->
@UserMembershipHandler.removeUser 'institution', @institution, @newUser._id, (error, user) =>
lastCall = @institution.update.lastCall
assertCalledWith(@institution.update, { $pull: managerIds: @newUser._id })
done()

View file

@ -0,0 +1,83 @@
chai = require('chai')
should = chai.should()
expect = require('chai').expect
sinon = require('sinon')
assertCalledWith = sinon.assert.calledWith
assertNotCalled = sinon.assert.notCalled
mongojs = require('mongojs')
ObjectId = mongojs.ObjectId
modulePath = "../../../../app/js/Features/UserMembership/UserMembershipViewModel"
SandboxedModule = require("sandboxed-module")
describe 'UserMembershipViewModel', ->
beforeEach ->
@UserGetter =
getUserOrUserStubById: sinon.stub()
@UserMembershipViewModel = SandboxedModule.require modulePath, requires:
'mongojs': mongojs
'../User/UserGetter': @UserGetter
@email = 'mock-email@bar.com'
@user = _id: 'mock-user-id', email: 'mock-email@baz.com', first_name: 'Name'
@userStub = _id: 'mock-user-stub-id', email: 'mock-stub-email@baz.com'
describe 'build', ->
it 'build email', ->
viewModel = @UserMembershipViewModel.build(@email)
expect(viewModel).to.deep.equal
email: @email
invite: true
first_name: null
last_name: null
_id: null
it 'build user', ->
viewModel = @UserMembershipViewModel.build(@user)
expect(viewModel._id).to.equal @user._id
expect(viewModel.email).to.equal @user.email
expect(viewModel.invite).to.equal false
describe 'build async', ->
beforeEach ->
@UserMembershipViewModel.build = sinon.stub()
it 'build email', (done) ->
@UserMembershipViewModel.buildAsync @email, (error, viewModel) =>
assertCalledWith(@UserMembershipViewModel.build, @email)
done()
it 'build user', (done) ->
@UserMembershipViewModel.buildAsync @user, (error, viewModel) =>
assertCalledWith(@UserMembershipViewModel.build, @user)
done()
it 'build user id', (done) ->
@UserGetter.getUserOrUserStubById.yields(null, @user, false)
@UserMembershipViewModel.buildAsync ObjectId(), (error, viewModel) =>
should.not.exist(error)
assertNotCalled(@UserMembershipViewModel.build)
expect(viewModel._id).to.equal @user._id
expect(viewModel.email).to.equal @user.email
expect(viewModel.first_name).to.equal @user.first_name
expect(viewModel.invite).to.equal false
should.exist(viewModel.email)
done()
it 'build user stub id', (done) ->
@UserGetter.getUserOrUserStubById.yields(null, @userStub, true)
@UserMembershipViewModel.buildAsync ObjectId(), (error, viewModel) =>
should.not.exist(error)
assertNotCalled(@UserMembershipViewModel.build)
expect(viewModel._id).to.equal @userStub._id
expect(viewModel.email).to.equal @userStub.email
expect(viewModel.invite).to.equal true
done()
it 'build user id with error', (done) ->
@UserGetter.getUserOrUserStubById.yields(new Error('nope'))
userId = ObjectId()
@UserMembershipViewModel.buildAsync userId, (error, viewModel) =>
should.not.exist(error)
assertNotCalled(@UserMembershipViewModel.build)
expect(viewModel._id).to.equal userId.toString()
should.not.exist(viewModel.email)
done()