[web] skip fetching members and invites for restricted users (#25673)

* [web] hide sensitive data from joinProject when building project view

* [web] skip fetching members and invites for restricted users

* [web] fix owner features in joinProject view

* [web] separate invited members from owner

* [web] skip fetching users with empty members  list

* [web] split await chain

Co-authored-by: Antoine Clausse <antoine.clausse@overleaf.com>

* [web] remove spurious parentheses

* [web] remove dead code

Co-authored-by: Antoine Clausse <antoine.clausse@overleaf.com>

---------

Co-authored-by: Antoine Clausse <antoine.clausse@overleaf.com>
GitOrigin-RevId: 5b4d874f974971e9c14d7412620805f8ebf63541
This commit is contained in:
Jakob Ackermann
2025-06-03 08:06:22 +00:00
committed by Copybot
co-authored by Antoine Clausse
parent 93c221775e
commit b45ffbf055
6 changed files with 268 additions and 189 deletions
@@ -16,9 +16,6 @@ module.exports = {
getMemberIdsWithPrivilegeLevels: callbackify(getMemberIdsWithPrivilegeLevels), getMemberIdsWithPrivilegeLevels: callbackify(getMemberIdsWithPrivilegeLevels),
getMemberIds: callbackify(getMemberIds), getMemberIds: callbackify(getMemberIds),
getInvitedMemberIds: callbackify(getInvitedMemberIds), getInvitedMemberIds: callbackify(getInvitedMemberIds),
getInvitedMembersWithPrivilegeLevels: callbackify(
getInvitedMembersWithPrivilegeLevels
),
getInvitedMembersWithPrivilegeLevelsFromFields: callbackify( getInvitedMembersWithPrivilegeLevelsFromFields: callbackify(
getInvitedMembersWithPrivilegeLevelsFromFields getInvitedMembersWithPrivilegeLevelsFromFields
), ),
@@ -36,7 +33,6 @@ module.exports = {
getMemberIdsWithPrivilegeLevels, getMemberIdsWithPrivilegeLevels,
getMemberIds, getMemberIds,
getInvitedMemberIds, getInvitedMemberIds,
getInvitedMembersWithPrivilegeLevels,
getInvitedMembersWithPrivilegeLevelsFromFields, getInvitedMembersWithPrivilegeLevelsFromFields,
getMemberIdPrivilegeLevel, getMemberIdPrivilegeLevel,
getInvitedEditCollaboratorCount, getInvitedEditCollaboratorCount,
@@ -95,11 +91,40 @@ class ProjectAccess {
this.#publicAccessLevel = project.publicAccesLevel this.#publicAccessLevel = project.publicAccesLevel
} }
/**
* @return {Promise<{ownerMember: LoadedProjectMember|undefined, members: LoadedProjectMember[]}>}
*/
async loadOwnerAndInvitedMembers() {
const all = await _loadMembers(
this.#members.filter(m => m.source !== Sources.TOKEN)
)
return {
ownerMember: all.find(m => m.privilegeLevel === PrivilegeLevels.OWNER),
members: all.filter(m => m.privilegeLevel !== PrivilegeLevels.OWNER),
}
}
/** /**
* @return {Promise<LoadedProjectMember[]>} * @return {Promise<LoadedProjectMember[]>}
*/ */
async loadInvitedMembers() { async loadInvitedMembers() {
return _loadMembers(this.#members.filter(m => m.source !== Sources.TOKEN)) return _loadMembers(
this.#members.filter(
m =>
m.source !== Sources.TOKEN &&
m.privilegeLevel !== PrivilegeLevels.OWNER
)
)
}
/**
* @return {Promise<LoadedProjectMember|undefined>}
*/
async loadOwner() {
const [owner] = await _loadMembers(
this.#members.filter(m => m.privilegeLevel === PrivilegeLevels.OWNER)
)
return owner
} }
/** /**
@@ -248,10 +273,6 @@ async function getInvitedMemberIds(projectId) {
return (await getProjectAccess(projectId)).invitedMemberIds() return (await getProjectAccess(projectId)).invitedMemberIds()
} }
async function getInvitedMembersWithPrivilegeLevels(projectId) {
return await (await getProjectAccess(projectId)).loadInvitedMembers()
}
async function getInvitedMembersWithPrivilegeLevelsFromFields( async function getInvitedMembersWithPrivilegeLevelsFromFields(
ownerId, ownerId,
collaboratorIds, collaboratorIds,
@@ -397,10 +418,9 @@ async function dangerouslyGetAllProjectsUserIsMemberOf(userId, fields) {
async function getAllInvitedMembers(projectId) { async function getAllInvitedMembers(projectId) {
try { try {
const { members } = ProjectEditorHandler.buildOwnerAndMembersViews( const projectAccess = await getProjectAccess(projectId)
await (await getProjectAccess(projectId)).loadInvitedMembers() const invitedMembers = await projectAccess.loadInvitedMembers()
) return invitedMembers.map(ProjectEditorHandler.buildUserModelView)
return members
} catch (err) { } catch (err) {
throw OError.tag(err, 'error getting members for project', { projectId }) throw OError.tag(err, 'error getting members for project', { projectId })
} }
@@ -526,6 +546,7 @@ function _getMemberIdsWithPrivilegeLevelsFromFields(
* @private * @private
*/ */
async function _loadMembers(members) { async function _loadMembers(members) {
if (members.length === 0) return []
const userIds = Array.from(new Set(members.map(m => m.id))) const userIds = Array.from(new Set(members.map(m => m.id)))
const users = new Map() const users = new Map()
for (const user of await UserGetter.promises.getUsers(userIds, { for (const user of await UserGetter.promises.getUsers(userIds, {
@@ -42,12 +42,6 @@ async function joinProject(req, res, next) {
if (!project) { if (!project) {
return res.sendStatus(403) return res.sendStatus(403)
} }
// Hide sensitive data if the user is restricted
if (isRestrictedUser) {
project.owner = { _id: project.owner._id }
project.members = []
project.invites = []
}
// Only show the 'renamed or deleted' message once // Only show the 'renamed or deleted' message once
if (project.deletedByExternalDataSource) { if (project.deletedByExternalDataSource) {
await ProjectDeleter.promises.unmarkAsDeletedByExternalSource(projectId) await ProjectDeleter.promises.unmarkAsDeletedByExternalSource(projectId)
@@ -75,7 +69,6 @@ async function _buildJoinProjectView(req, projectId, userId) {
throw new Errors.NotFoundError('project not found') throw new Errors.NotFoundError('project not found')
} }
const projectAccess = new ProjectAccess(project) const projectAccess = new ProjectAccess(project)
const members = await projectAccess.loadInvitedMembers()
const token = req.body.anonymousAccessToken const token = req.body.anonymousAccessToken
const privilegeLevel = const privilegeLevel =
await AuthorizationManager.promises.getPrivilegeLevelForProjectWithProjectAccess( await AuthorizationManager.promises.getPrivilegeLevelForProjectWithProjectAccess(
@@ -87,8 +80,6 @@ async function _buildJoinProjectView(req, projectId, userId) {
if (privilegeLevel == null || privilegeLevel === PrivilegeLevels.NONE) { if (privilegeLevel == null || privilegeLevel === PrivilegeLevels.NONE) {
return { project: null, privilegeLevel: null, isRestrictedUser: false } return { project: null, privilegeLevel: null, isRestrictedUser: false }
} }
const invites =
await CollaboratorsInviteGetter.promises.getAllInvites(projectId)
const isTokenMember = projectAccess.isUserTokenMember(userId) const isTokenMember = projectAccess.isUserTokenMember(userId)
const isInvitedMember = projectAccess.isUserInvitedMember(userId) const isInvitedMember = projectAccess.isUserInvitedMember(userId)
const isRestrictedUser = AuthorizationManager.isRestrictedUser( const isRestrictedUser = AuthorizationManager.isRestrictedUser(
@@ -97,11 +88,23 @@ async function _buildJoinProjectView(req, projectId, userId) {
isTokenMember, isTokenMember,
isInvitedMember isInvitedMember
) )
let ownerMember
let members = []
let invites = []
if (isRestrictedUser) {
ownerMember = await projectAccess.loadOwner()
} else {
;({ ownerMember, members } =
await projectAccess.loadOwnerAndInvitedMembers())
invites = await CollaboratorsInviteGetter.promises.getAllInvites(projectId)
}
return { return {
project: ProjectEditorHandler.buildProjectModelView( project: ProjectEditorHandler.buildProjectModelView(
project, project,
ownerMember,
members, members,
invites invites,
isRestrictedUser
), ),
privilegeLevel, privilegeLevel,
isTokenMember, isTokenMember,
@@ -6,8 +6,13 @@ const Features = require('../../infrastructure/Features')
module.exports = ProjectEditorHandler = { module.exports = ProjectEditorHandler = {
trackChangesAvailable: false, trackChangesAvailable: false,
buildProjectModelView(project, members, invites) { buildProjectModelView(
let owner, ownerFeatures project,
ownerMember,
members,
invites,
isRestrictedUser
) {
const result = { const result = {
_id: project._id, _id: project._id,
name: project.name, name: project.name,
@@ -20,20 +25,23 @@ module.exports = ProjectEditorHandler = {
description: project.description, description: project.description,
spellCheckLanguage: project.spellCheckLanguage, spellCheckLanguage: project.spellCheckLanguage,
deletedByExternalDataSource: project.deletedByExternalDataSource || false, deletedByExternalDataSource: project.deletedByExternalDataSource || false,
members: [],
invites: this.buildInvitesView(invites),
imageName: imageName:
project.imageName != null project.imageName != null
? Path.basename(project.imageName) ? Path.basename(project.imageName)
: undefined, : undefined,
} }
;({ owner, ownerFeatures, members } = if (isRestrictedUser) {
this.buildOwnerAndMembersViews(members)) result.owner = { _id: project.owner_ref }
result.owner = owner result.members = []
result.members = members result.invites = []
} else {
result.owner = this.buildUserModelView(ownerMember)
result.members = members.map(this.buildUserModelView)
result.invites = this.buildInvitesView(invites)
}
result.features = _.defaults(ownerFeatures || {}, { result.features = _.defaults(ownerMember?.user?.features || {}, {
collaborators: -1, // Infinite collaborators: -1, // Infinite
versioning: false, versioning: false,
dropbox: false, dropbox: false,
@@ -62,25 +70,6 @@ module.exports = ProjectEditorHandler = {
return result return result
}, },
buildOwnerAndMembersViews(members) {
let owner = null
let ownerFeatures = null
const filteredMembers = []
for (const member of members || []) {
if (member.privilegeLevel === 'owner') {
ownerFeatures = member.user.features
owner = this.buildUserModelView(member)
} else {
filteredMembers.push(this.buildUserModelView(member))
}
}
return {
owner,
ownerFeatures,
members: filteredMembers,
}
},
buildUserModelView(member) { buildUserModelView(member) {
const user = member.user const user = member.user
return { return {
@@ -62,7 +62,7 @@ describe('CollaboratorsGetter', function () {
}, },
} }
this.ProjectEditorHandler = { this.ProjectEditorHandler = {
buildOwnerAndMembersViews: sinon.stub(), buildUserModelView: sinon.stub(),
} }
this.CollaboratorsGetter = SandboxedModule.require(MODULE_PATH, { this.CollaboratorsGetter = SandboxedModule.require(MODULE_PATH, {
requires: { requires: {
@@ -204,30 +204,6 @@ describe('CollaboratorsGetter', function () {
}) })
}) })
describe('getInvitedMembersWithPrivilegeLevels', function () {
beforeEach(function () {
this.UserGetter.promises.getUsers.resolves([
{ _id: this.readOnlyRef1 },
{ _id: this.readOnlyTokenRef },
{ _id: this.readWriteRef2 },
{ _id: this.readWriteTokenRef },
{ _id: this.reviewer1Ref },
])
})
it('should return an array of invited members with their privilege levels', async function () {
const result =
await this.CollaboratorsGetter.promises.getInvitedMembersWithPrivilegeLevels(
this.project._id
)
expect(result).to.have.deep.members([
{ user: { _id: this.readOnlyRef1 }, privilegeLevel: 'readOnly' },
{ user: { _id: this.readWriteRef2 }, privilegeLevel: 'readAndWrite' },
{ user: { _id: this.reviewer1Ref }, privilegeLevel: 'review' },
])
})
})
describe('getMemberIdPrivilegeLevel', function () { describe('getMemberIdPrivilegeLevel', function () {
it('should return the privilege level if it exists', async function () { it('should return the privilege level if it exists', async function () {
const level = const level =
@@ -401,20 +377,21 @@ describe('CollaboratorsGetter', function () {
{ user: this.readWriteUser, privilegeLevel: 'readAndWrite' }, { user: this.readWriteUser, privilegeLevel: 'readAndWrite' },
{ user: this.reviewUser, privilegeLevel: 'review' }, { user: this.reviewUser, privilegeLevel: 'review' },
] ]
this.views = { this.memberViews = [
owner: this.owningUser, { _id: this.readWriteUser._id, email: this.readWriteUser.email },
ownerFeatures: this.owningUser.features, { _id: this.reviewUser._id, email: this.reviewUser.email },
members: [ ]
{ _id: this.readWriteUser._id, email: this.readWriteUser.email },
{ _id: this.reviewUser._id, email: this.reviewUser.email },
],
}
this.UserGetter.promises.getUsers.resolves([ this.UserGetter.promises.getUsers.resolves([
this.owningUser, this.owningUser,
this.readWriteUser, this.readWriteUser,
this.reviewUser, this.reviewUser,
]) ])
this.ProjectEditorHandler.buildOwnerAndMembersViews.returns(this.views) this.ProjectEditorHandler.buildUserModelView
.withArgs(this.members[1])
.returns(this.memberViews[0])
this.ProjectEditorHandler.buildUserModelView
.withArgs(this.members[2])
.returns(this.memberViews[1])
this.result = this.result =
await this.CollaboratorsGetter.promises.getAllInvitedMembers( await this.CollaboratorsGetter.promises.getAllInvitedMembers(
this.project._id this.project._id
@@ -422,15 +399,18 @@ describe('CollaboratorsGetter', function () {
}) })
it('should produce a list of members', function () { it('should produce a list of members', function () {
expect(this.result).to.deep.equal(this.views.members) expect(this.result).to.deep.equal(this.memberViews)
}) })
it('should call ProjectEditorHandler.buildOwnerAndMembersViews', function () { it('should call ProjectEditorHandler.buildUserModelView', function () {
expect(this.ProjectEditorHandler.buildOwnerAndMembersViews).to.have.been expect(this.ProjectEditorHandler.buildUserModelView).to.have.been
.calledOnce .calledTwice
expect( expect(
this.ProjectEditorHandler.buildOwnerAndMembersViews this.ProjectEditorHandler.buildUserModelView
).to.have.been.calledWith(this.members) ).to.have.been.calledWith(this.members[1])
expect(
this.ProjectEditorHandler.buildUserModelView
).to.have.been.calledWith(this.members[2])
}) })
}) })
@@ -20,6 +20,12 @@ describe('EditorHttpController', function () {
_id: new ObjectId(), _id: new ObjectId(),
projects: {}, projects: {},
} }
this.members = [
{ user: { _id: 'owner', features: {} }, privilegeLevel: 'owner' },
{ user: { _id: 'one' }, privilegeLevel: 'readOnly' },
]
this.ownerMember = this.members[0]
this.invites = [{ _id: 'three' }, { _id: 'four' }]
this.projectView = { this.projectView = {
_id: this.project._id, _id: this.project._id,
owner: { owner: {
@@ -27,7 +33,10 @@ describe('EditorHttpController', function () {
email: 'owner@example.com', email: 'owner@example.com',
other_property: true, other_property: true,
}, },
members: [{ one: 1 }, { two: 2 }], members: [
{ _id: 'owner', privileges: 'owner' },
{ _id: 'one', privileges: 'readOnly' },
],
invites: [{ three: 3 }, { four: 4 }], invites: [{ three: 3 }, { four: 4 }],
} }
this.reducedProjectView = { this.reducedProjectView = {
@@ -56,10 +65,16 @@ describe('EditorHttpController', function () {
.resolves('owner'), .resolves('owner'),
}, },
} }
const members = this.members
const ownerMember = this.ownerMember
this.CollaboratorsGetter = { this.CollaboratorsGetter = {
ProjectAccess: class { ProjectAccess: class {
loadInvitedMembers() { loadOwnerAndInvitedMembers() {
return [] return { members, ownerMember }
}
loadOwner() {
return ownerMember
} }
isUserTokenMember() { isUserTokenMember() {
@@ -71,9 +86,6 @@ describe('EditorHttpController', function () {
} }
}, },
promises: { promises: {
getInvitedMembersWithPrivilegeLevels: sinon
.stub()
.resolves(['members', 'mock']),
isUserInvitedMemberOfProject: sinon.stub().resolves(false), isUserInvitedMemberOfProject: sinon.stub().resolves(false),
}, },
} }
@@ -82,22 +94,23 @@ describe('EditorHttpController', function () {
userIsTokenMember: sinon.stub().resolves(false), userIsTokenMember: sinon.stub().resolves(false),
}, },
} }
this.invites = [
{
_id: 'invite_one',
email: 'user-one@example.com',
privileges: 'readOnly',
projectId: this.project._id,
},
{
_id: 'invite_two',
email: 'user-two@example.com',
privileges: 'readOnly',
projectId: this.project._id,
},
]
this.CollaboratorsInviteGetter = { this.CollaboratorsInviteGetter = {
promises: { promises: {
getAllInvites: sinon.stub().resolves([ getAllInvites: sinon.stub().resolves(this.invites),
{
_id: 'invite_one',
email: 'user-one@example.com',
privileges: 'readOnly',
projectId: this.project._id,
},
{
_id: 'invite_two',
email: 'user-two@example.com',
privileges: 'readOnly',
projectId: this.project._id,
},
]),
}, },
} }
this.EditorController = { this.EditorController = {
@@ -195,6 +208,18 @@ describe('EditorHttpController', function () {
this.EditorHttpController.joinProject(this.req, this.res) this.EditorHttpController.joinProject(this.req, this.res)
}) })
it('should request a full view', function () {
expect(
this.ProjectEditorHandler.buildProjectModelView
).to.have.been.calledWith(
this.project,
this.ownerMember,
this.members,
this.invites,
false
)
})
it('should return the project and privilege level', function () { it('should return the project and privilege level', function () {
expect(this.res.json).to.have.been.calledWith({ expect(this.res.json).to.have.been.calledWith({
project: this.projectView, project: this.projectView,
@@ -231,6 +256,9 @@ describe('EditorHttpController', function () {
describe('with a restricted user', function () { describe('with a restricted user', function () {
beforeEach(function (done) { beforeEach(function (done) {
this.ProjectEditorHandler.buildProjectModelView.returns(
this.reducedProjectView
)
this.AuthorizationManager.isRestrictedUser.returns(true) this.AuthorizationManager.isRestrictedUser.returns(true)
this.AuthorizationManager.promises.getPrivilegeLevelForProjectWithProjectAccess.resolves( this.AuthorizationManager.promises.getPrivilegeLevelForProjectWithProjectAccess.resolves(
'readOnly' 'readOnly'
@@ -239,6 +267,12 @@ describe('EditorHttpController', function () {
this.EditorHttpController.joinProject(this.req, this.res) this.EditorHttpController.joinProject(this.req, this.res)
}) })
it('should request a restricted view', function () {
expect(
this.ProjectEditorHandler.buildProjectModelView
).to.have.been.calledWith(this.project, this.ownerMember, [], [], true)
})
it('should mark the user as restricted, and hide details of owner', function () { it('should mark the user as restricted, and hide details of owner', function () {
expect(this.res.json).to.have.been.calledWith({ expect(this.res.json).to.have.been.calledWith({
project: this.reducedProjectView, project: this.reducedProjectView,
@@ -268,6 +302,9 @@ describe('EditorHttpController', function () {
beforeEach(function (done) { beforeEach(function (done) {
this.token = 'token' this.token = 'token'
this.TokenAccessHandler.getRequestToken.returns(this.token) this.TokenAccessHandler.getRequestToken.returns(this.token)
this.ProjectEditorHandler.buildProjectModelView.returns(
this.reducedProjectView
)
this.req.body = { this.req.body = {
userId: 'anonymous-user', userId: 'anonymous-user',
anonymousAccessToken: this.token, anonymousAccessToken: this.token,
@@ -282,6 +319,12 @@ describe('EditorHttpController', function () {
this.EditorHttpController.joinProject(this.req, this.res) this.EditorHttpController.joinProject(this.req, this.res)
}) })
it('should request a restricted view', function () {
expect(
this.ProjectEditorHandler.buildProjectModelView
).to.have.been.calledWith(this.project, this.ownerMember, [], [], true)
})
it('should mark the user as restricted', function () { it('should mark the user as restricted', function () {
expect(this.res.json).to.have.been.calledWith({ expect(this.res.json).to.have.been.calledWith({
project: this.reducedProjectView, project: this.reducedProjectView,
@@ -8,6 +8,7 @@ describe('ProjectEditorHandler', function () {
beforeEach(function () { beforeEach(function () {
this.project = { this.project = {
_id: 'project-id', _id: 'project-id',
owner_ref: 'owner-id',
name: 'Project Name', name: 'Project Name',
rootDoc_id: 'file-id', rootDoc_id: 'file-id',
publicAccesLevel: 'private', publicAccesLevel: 'private',
@@ -43,16 +44,19 @@ describe('ProjectEditorHandler', function () {
}, },
], ],
} }
this.ownerMember = {
user: (this.owner = {
_id: 'owner-id',
first_name: 'Owner',
last_name: 'Overleaf',
email: 'owner@overleaf.com',
features: {
compileTimeout: 240,
},
}),
privilegeLevel: 'owner',
}
this.members = [ this.members = [
{
user: (this.owner = {
_id: 'owner-id',
first_name: 'Owner',
last_name: 'Overleaf',
email: 'owner@overleaf.com',
}),
privilegeLevel: 'owner',
},
{ {
user: { user: {
_id: 'read-only-id', _id: 'read-only-id',
@@ -96,8 +100,10 @@ describe('ProjectEditorHandler', function () {
beforeEach(function () { beforeEach(function () {
this.result = this.handler.buildProjectModelView( this.result = this.handler.buildProjectModelView(
this.project, this.project,
this.ownerMember,
this.members, this.members,
this.invites this.invites,
false
) )
}) })
@@ -206,6 +212,93 @@ describe('ProjectEditorHandler', function () {
expect(invite.token).not.to.exist expect(invite.token).not.to.exist
} }
}) })
it('should have the correct features', function () {
expect(this.result.features.compileTimeout).to.equal(240)
})
})
describe('with a restricted user', function () {
beforeEach(function () {
this.result = this.handler.buildProjectModelView(
this.project,
this.ownerMember,
[],
[],
true
)
})
it('should include the id', function () {
expect(this.result._id).to.exist
this.result._id.should.equal('project-id')
})
it('should include the name', function () {
expect(this.result.name).to.exist
this.result.name.should.equal('Project Name')
})
it('should include the root doc id', function () {
expect(this.result.rootDoc_id).to.exist
this.result.rootDoc_id.should.equal('file-id')
})
it('should include the public access level', function () {
expect(this.result.publicAccesLevel).to.exist
this.result.publicAccesLevel.should.equal('private')
})
it('should hide the owner', function () {
expect(this.result.owner).to.deep.equal({ _id: 'owner-id' })
})
it('should hide members', function () {
this.result.members.length.should.equal(0)
})
it('should include folders in the project', function () {
this.result.rootFolder[0]._id.should.equal('root-folder-id')
this.result.rootFolder[0].name.should.equal('')
this.result.rootFolder[0].folders[0]._id.should.equal('sub-folder-id')
this.result.rootFolder[0].folders[0].name.should.equal('folder')
})
it('should not duplicate folder contents', function () {
this.result.rootFolder[0].docs.length.should.equal(0)
this.result.rootFolder[0].fileRefs.length.should.equal(0)
})
it('should include files in the project', function () {
this.result.rootFolder[0].folders[0].fileRefs[0]._id.should.equal(
'file-id'
)
this.result.rootFolder[0].folders[0].fileRefs[0].name.should.equal(
'image.png'
)
this.result.rootFolder[0].folders[0].fileRefs[0].created.should.equal(
this.created
)
expect(this.result.rootFolder[0].folders[0].fileRefs[0].size).not.to
.exist
})
it('should include docs in the project but not the lines', function () {
this.result.rootFolder[0].folders[0].docs[0]._id.should.equal('doc-id')
this.result.rootFolder[0].folders[0].docs[0].name.should.equal(
'main.tex'
)
expect(this.result.rootFolder[0].folders[0].docs[0].lines).not.to.exist
})
it('should hide invites', function () {
expect(this.result.invites).to.have.length(0)
})
it('should have the correct features', function () {
expect(this.result.features.compileTimeout).to.equal(240)
})
}) })
describe('deletedByExternalDataSource', function () { describe('deletedByExternalDataSource', function () {
@@ -213,8 +306,10 @@ describe('ProjectEditorHandler', function () {
delete this.project.deletedByExternalDataSource delete this.project.deletedByExternalDataSource
const result = this.handler.buildProjectModelView( const result = this.handler.buildProjectModelView(
this.project, this.project,
this.ownerMember,
this.members, this.members,
[] [],
false
) )
result.deletedByExternalDataSource.should.equal(false) result.deletedByExternalDataSource.should.equal(false)
}) })
@@ -222,8 +317,10 @@ describe('ProjectEditorHandler', function () {
it('should set the deletedByExternalDataSource flag to false when it is false', function () { it('should set the deletedByExternalDataSource flag to false when it is false', function () {
const result = this.handler.buildProjectModelView( const result = this.handler.buildProjectModelView(
this.project, this.project,
this.ownerMember,
this.members, this.members,
[] [],
false
) )
result.deletedByExternalDataSource.should.equal(false) result.deletedByExternalDataSource.should.equal(false)
}) })
@@ -232,8 +329,10 @@ describe('ProjectEditorHandler', function () {
this.project.deletedByExternalDataSource = true this.project.deletedByExternalDataSource = true
const result = this.handler.buildProjectModelView( const result = this.handler.buildProjectModelView(
this.project, this.project,
this.ownerMember,
this.members, this.members,
[] [],
false
) )
result.deletedByExternalDataSource.should.equal(true) result.deletedByExternalDataSource.should.equal(true)
}) })
@@ -249,8 +348,10 @@ describe('ProjectEditorHandler', function () {
} }
this.result = this.handler.buildProjectModelView( this.result = this.handler.buildProjectModelView(
this.project, this.project,
this.ownerMember,
this.members, this.members,
[] [],
false
) )
}) })
@@ -278,8 +379,10 @@ describe('ProjectEditorHandler', function () {
} }
this.result = this.handler.buildProjectModelView( this.result = this.handler.buildProjectModelView(
this.project, this.project,
this.ownerMember,
this.members, this.members,
[] [],
false
) )
}) })
it('should not emit trackChangesState', function () { it('should not emit trackChangesState', function () {
@@ -302,8 +405,10 @@ describe('ProjectEditorHandler', function () {
this.project.track_changes = dbEntry this.project.track_changes = dbEntry
this.result = this.handler.buildProjectModelView( this.result = this.handler.buildProjectModelView(
this.project, this.project,
this.ownerMember,
this.members, this.members,
[] [],
false
) )
}) })
it(`should set trackChangesState=${expected}`, function () { it(`should set trackChangesState=${expected}`, function () {
@@ -322,66 +427,4 @@ describe('ProjectEditorHandler', function () {
}) })
}) })
}) })
describe('buildOwnerAndMembersViews', function () {
beforeEach(function () {
this.owner.features = {
versioning: true,
collaborators: 3,
compileGroup: 'priority',
compileTimeout: 22,
}
this.result = this.handler.buildOwnerAndMembersViews(this.members)
})
it('should produce an object with the right keys', function () {
expect(this.result).to.have.all.keys([
'owner',
'ownerFeatures',
'members',
])
})
it('should separate the owner from the members', function () {
this.result.members.length.should.equal(this.members.length - 1)
expect(this.result.owner._id).to.equal(this.owner._id)
expect(this.result.owner.email).to.equal(this.owner.email)
expect(
this.result.members.filter(m => m._id === this.owner._id).length
).to.equal(0)
})
it('should extract the ownerFeatures from the owner object', function () {
expect(this.result.ownerFeatures).to.deep.equal(this.owner.features)
})
describe('when there is no owner', function () {
beforeEach(function () {
// remove the owner from members list
this.membersWithoutOwner = this.members.filter(
m => m.user._id !== this.owner._id
)
this.result = this.handler.buildOwnerAndMembersViews(
this.membersWithoutOwner
)
})
it('should produce an object with the right keys', function () {
expect(this.result).to.have.all.keys([
'owner',
'ownerFeatures',
'members',
])
})
it('should not separate out an owner', function () {
this.result.members.length.should.equal(this.membersWithoutOwner.length)
expect(this.result.owner).to.equal(null)
})
it('should not extract the ownerFeatures from the owner object', function () {
expect(this.result.ownerFeatures).to.equal(null)
})
})
})
}) })