Merge pull request #24433 from overleaf/em-pending-reviewers

Support reviewers in the collaborator limit enforcement logic

GitOrigin-RevId: f11a8e37ca6ef36f9894233803c6ee8363bf0ff8
This commit is contained in:
Eric Mc Sween
2025-03-27 08:33:08 -04:00
committed by Copybot
parent 373b222929
commit f46fd6f2d5
17 changed files with 268 additions and 63 deletions
@@ -60,6 +60,7 @@ async function getMemberIdsWithPrivilegeLevels(projectId) {
publicAccesLevel: 1,
pendingEditor_refs: 1,
reviewer_refs: 1,
pendingReviewer_refs: 1,
})
if (!project) {
throw new Errors.NotFoundError(`no project found with id ${projectId}`)
@@ -72,7 +73,8 @@ async function getMemberIdsWithPrivilegeLevels(projectId) {
project.tokenAccessReadOnly_refs,
project.publicAccesLevel,
project.pendingEditor_refs,
project.reviewer_refs
project.reviewer_refs,
project.pendingReviewer_refs
)
return memberIds
}
@@ -107,7 +109,8 @@ async function getInvitedMembersWithPrivilegeLevelsFromFields(
[],
null,
[],
reviewerIds
reviewerIds,
[]
)
return _loadMembers(members)
}
@@ -139,13 +142,14 @@ async function getInvitedEditCollaboratorCount(projectId) {
}
async function getInvitedPendingEditorCount(projectId) {
// Only counts invited members that are readonly pending editors
// Only counts invited members that are readonly pending editors or pending
// reviewers
const members = await getMemberIdsWithPrivilegeLevels(projectId)
return members.filter(
m =>
m.source === Sources.INVITE &&
m.privilegeLevel === PrivilegeLevels.READ_ONLY &&
m.pendingEditor === true
(m.pendingEditor || m.pendingReviewer)
).length
}
@@ -320,7 +324,8 @@ function _getMemberIdsWithPrivilegeLevelsFromFields(
tokenAccessReadOnlyIds,
publicAccessLevel,
pendingEditorIds,
reviewerIds
reviewerIds,
pendingReviewerIds
) {
const members = []
members.push({
@@ -328,6 +333,7 @@ function _getMemberIdsWithPrivilegeLevelsFromFields(
privilegeLevel: PrivilegeLevels.OWNER,
source: Sources.OWNER,
})
for (const memberId of collaboratorIds || []) {
members.push({
id: memberId.toString(),
@@ -335,16 +341,22 @@ function _getMemberIdsWithPrivilegeLevelsFromFields(
source: Sources.INVITE,
})
}
for (const memberId of readOnlyIds || []) {
members.push({
const record = {
id: memberId.toString(),
privilegeLevel: PrivilegeLevels.READ_ONLY,
source: Sources.INVITE,
...(pendingEditorIds?.some(pe => memberId.equals(pe)) && {
pendingEditor: true,
}),
})
}
if (pendingEditorIds?.some(pe => memberId.equals(pe))) {
record.pendingEditor = true
} else if (pendingReviewerIds?.some(pr => memberId.equals(pr))) {
record.pendingReviewer = true
}
members.push(record)
}
if (publicAccessLevel === PublicAccessLevels.TOKEN_BASED) {
for (const memberId of tokenAccessIds || []) {
members.push({
@@ -361,6 +373,7 @@ function _getMemberIdsWithPrivilegeLevelsFromFields(
})
}
}
for (const memberId of reviewerIds || []) {
members.push({
id: memberId.toString(),
@@ -385,11 +398,16 @@ async function _loadMembers(members) {
signUpDate: 1,
})
if (user != null) {
return {
const record = {
user,
privilegeLevel: member.privilegeLevel,
...(member.pendingEditor && { pendingEditor: true }),
}
if (member.pendingEditor) {
record.pendingEditor = true
} else if (member.pendingReviewer) {
record.pendingReviewer = true
}
return record
} else {
return null
}
@@ -53,6 +53,7 @@ async function removeUserFromProject(projectId, userId) {
reviewer_refs: userId,
readOnly_refs: userId,
pendingEditor_refs: userId,
pendingReviewer_refs: userId,
tokenAccessReadOnly_refs: userId,
tokenAccessReadAndWrite_refs: userId,
trashed: userId,
@@ -68,6 +69,7 @@ async function removeUserFromProject(projectId, userId) {
readOnly_refs: userId,
reviewer_refs: userId,
pendingEditor_refs: userId,
pendingReviewer_refs: userId,
tokenAccessReadOnly_refs: userId,
tokenAccessReadAndWrite_refs: userId,
archived: userId,
@@ -106,7 +108,7 @@ async function addUserIdToProject(
addingUserId,
userId,
privilegeLevel,
{ pendingEditor } = {}
{ pendingEditor, pendingReviewer } = {}
) {
const project = await ProjectGetter.promises.getProject(projectId, {
owner_ref: 1,
@@ -133,9 +135,17 @@ async function addUserIdToProject(
level = { readOnly_refs: userId }
if (pendingEditor) {
level.pendingEditor_refs = userId
} else if (pendingReviewer) {
level.pendingReviewer_refs = userId
}
logger.debug(
{ privileges: 'readOnly', userId, projectId, pendingEditor },
{
privileges: 'readOnly',
userId,
projectId,
pendingEditor,
pendingReviewer,
},
'adding user'
)
} else if (privilegeLevel === PrivilegeLevels.REVIEW) {
@@ -246,6 +256,19 @@ async function transferProjects(fromUserId, toUserId) {
}
).exec()
await Project.updateMany(
{ pendingReviewer_refs: fromUserId },
{
$addToSet: { pendingReviewer_refs: toUserId },
}
).exec()
await Project.updateMany(
{ pendingReviewer_refs: fromUserId },
{
$pull: { pendingReviewer_refs: fromUserId },
}
).exec()
// Flush in background, no need to block on this
_flushProjects(projectIds).catch(err => {
logger.err(
@@ -259,7 +282,7 @@ async function setCollaboratorPrivilegeLevel(
projectId,
userId,
privilegeLevel,
{ pendingEditor } = {}
{ pendingEditor, pendingReviewer } = {}
) {
// Make sure we're only updating the project if the user is already a
// collaborator
@@ -279,6 +302,7 @@ async function setCollaboratorPrivilegeLevel(
readOnly_refs: userId,
pendingEditor_refs: userId,
reviewer_refs: userId,
pendingReviewer_refs: userId,
},
$addToSet: { collaberator_refs: userId },
}
@@ -290,6 +314,7 @@ async function setCollaboratorPrivilegeLevel(
readOnly_refs: userId,
pendingEditor_refs: userId,
collaberator_refs: userId,
pendingReviewer_refs: userId,
},
$addToSet: { reviewer_refs: userId },
}
@@ -316,11 +341,19 @@ async function setCollaboratorPrivilegeLevel(
$pull: { collaberator_refs: userId, reviewer_refs: userId },
$addToSet: { readOnly_refs: userId },
}
if (pendingEditor) {
update.$addToSet.pendingEditor_refs = userId
} else {
update.$pull.pendingEditor_refs = userId
}
if (pendingReviewer) {
update.$addToSet.pendingReviewer_refs = userId
} else {
update.$pull.pendingReviewer_refs = userId
}
break
}
default: {
@@ -152,33 +152,52 @@ const CollaboratorsInviteHandler = {
const project = await ProjectGetter.promises.getProject(projectId, {
owner_ref: 1,
})
const pendingEditor =
invite.privileges === PrivilegeLevels.READ_AND_WRITE &&
!(await LimitationsManager.promises.canAcceptEditCollaboratorInvite(
project._id
))
if (pendingEditor) {
logger.debug(
{ projectId, userId: user._id },
'no collaborator slots available, user added as read only (pending editor)'
let privilegeLevel = invite.privileges
const opts = {}
if (
[PrivilegeLevels.READ_AND_WRITE, PrivilegeLevels.REVIEW].includes(
invite.privileges
)
await ProjectAuditLogHandler.promises.addEntry(
projectId,
'editor-moved-to-pending', // controller already logged accept-invite
null,
null,
{
userId: user._id.toString(),
) {
const allowed =
await LimitationsManager.promises.canAcceptEditCollaboratorInvite(
project._id
)
if (!allowed) {
privilegeLevel = PrivilegeLevels.READ_ONLY
if (invite.privileges === PrivilegeLevels.READ_AND_WRITE) {
opts.pendingEditor = true
} else if (invite.privileges === PrivilegeLevels.REVIEW) {
opts.pendingReviewer = true
}
)
logger.debug(
{ projectId, userId: user._id, privileges: invite.privileges },
'no collaborator slots available, user added as read only (pending editor)'
)
await ProjectAuditLogHandler.promises.addEntry(
projectId,
'editor-moved-to-pending', // controller already logged accept-invite
null,
null,
{
userId: user._id.toString(),
role:
invite.privileges === PrivilegeLevels.REVIEW
? 'reviewer'
: 'editor',
}
)
}
}
await CollaboratorsHandler.promises.addUserIdToProject(
projectId,
invite.sendingUserId,
user._id,
pendingEditor ? PrivilegeLevels.READ_ONLY : invite.privileges,
{ pendingEditor }
privilegeLevel,
opts
)
// Remove invite
@@ -107,6 +107,7 @@ module.exports = ProjectEditorHandler = {
privileges: member.privilegeLevel,
signUpDate: user.signUpDate,
pendingEditor: member.pendingEditor,
pendingReviewer: member.pendingReviewer,
}
},
@@ -84,9 +84,8 @@ async function canChangeCollaboratorPrivilegeLevel(
projectId
)
if (
[PrivilegeLevels.READ_AND_WRITE, PrivilegeLevels.REVIEW].includes(
currentPrivilegeLevel
)
currentPrivilegeLevel === PrivilegeLevels.READ_AND_WRITE ||
currentPrivilegeLevel === PrivilegeLevels.REVIEW
) {
// Current collaborator already takes a slot, so changing the privilege
// level won't increase the collaborator count
+1
View File
@@ -41,6 +41,7 @@ const ProjectSchema = new Schema(
reviewer_refs: [{ type: ObjectId, ref: 'User' }],
readOnly_refs: [{ type: ObjectId, ref: 'User' }],
pendingEditor_refs: [{ type: ObjectId, ref: 'User' }],
pendingReviewer_refs: [{ type: ObjectId, ref: 'User' }],
rootDoc_id: { type: ObjectId },
rootFolder: [FolderSchema],
mainBibliographyDoc_id: { type: ObjectId },
@@ -1975,6 +1975,7 @@
"view_more": "",
"view_only_access": "",
"view_only_downgraded": "",
"view_only_reviewer_downgraded": "",
"view_options": "",
"view_pdf": "",
"view_your_invoices": "",
@@ -73,7 +73,10 @@ export default function EditMember({
}
function shouldWarnMember() {
return hasExceededCollaboratorLimit && privileges === 'readAndWrite'
return (
hasExceededCollaboratorLimit &&
['readAndWrite', 'review'].includes(privileges)
)
}
function commitPrivilegeChange(newPrivileges: PermissionsOption) {
@@ -145,12 +148,16 @@ export default function EditMember({
<div className="project-member-email-icon">
<MaterialIcon
type={
shouldWarnMember() || member.pendingEditor
shouldWarnMember() ||
member.pendingEditor ||
member.pendingReviewer
? 'warning'
: 'person'
}
className={
shouldWarnMember() || member.pendingEditor
shouldWarnMember() ||
member.pendingEditor ||
member.pendingReviewer
? 'project-member-warning'
: undefined
}
@@ -160,6 +167,11 @@ export default function EditMember({
{member.pendingEditor && (
<div className="subtitle">{t('view_only_downgraded')}</div>
)}
{member.pendingReviewer && (
<div className="subtitle">
{t('view_only_reviewer_downgraded')}
</div>
)}
{shouldWarnMember() && (
<div className="subtitle">
{t('will_lose_edit_access_on_date', {
@@ -41,8 +41,10 @@ export default function ShareModalBody() {
// for moving between warning and info notification states etc.
const somePendingEditorsResolved = useMemo(() => {
return (
members.some(member => member.privileges === 'readAndWrite') &&
members.some(member => member.pendingEditor)
members.some(member =>
['readAndWrite', 'review'].includes(member.privileges)
) &&
members.some(member => member.pendingEditor || member.pendingReviewer)
)
}, [members])
@@ -54,7 +56,9 @@ export default function ShareModalBody() {
if (features.collaborators === -1) {
return false
}
return members.some(member => member.pendingEditor)
return members.some(
member => member.pendingEditor || member.pendingReviewer
)
}, [features, isProjectOwner, members])
const hasExceededCollaboratorLimit = useMemo(() => {
@@ -76,8 +80,13 @@ export default function ShareModalBody() {
return [
...members.filter(member => member.privileges === 'readAndWrite'),
...members.filter(member => member.pendingEditor),
...members.filter(member => member.privileges === 'review'),
...members.filter(member => member.pendingReviewer),
...members.filter(
member => !member.pendingEditor && member.privileges !== 'readAndWrite'
member =>
!member.pendingEditor &&
!member.pendingReviewer &&
!['readAndWrite', 'review'].includes(member.privileges)
),
]
}, [members])
@@ -104,7 +113,9 @@ export default function ShareModalBody() {
key={member._id}
member={member}
hasExceededCollaboratorLimit={hasExceededCollaboratorLimit}
hasBeenDowngraded={member.pendingEditor ?? false}
hasBeenDowngraded={Boolean(
member.pendingEditor || member.pendingReviewer
)}
canAddCollaborators={canAddCollaborators}
/>
) : (
@@ -78,9 +78,12 @@ const ShareProjectModal = React.memo(function ShareProjectModal({
return false
}
return (
project.members.filter(member => member.privileges === 'readAndWrite')
.length > (project.features.collaborators ?? 1) ||
project.members.some(member => member.pendingEditor)
project.members.filter(member =>
['readAndWrite', 'review'].includes(member.privileges)
).length > (project.features.collaborators ?? 1) ||
project.members.some(
member => member.pendingEditor || member.pendingReviewer
)
)
}
@@ -116,7 +116,11 @@ export const EditorProvider: FC = ({ children }) => {
const isPendingEditor = useMemo(
() =>
members?.some(member => member._id === userId && member.pendingEditor),
members?.some(
member =>
member._id === userId &&
(member.pendingEditor || member.pendingReviewer)
),
[members, userId]
)
@@ -9,6 +9,7 @@ export type ProjectContextMember = {
first_name: string
last_name: string
pendingEditor?: boolean
pendingReviewer?: boolean
}
export type ProjectContextValue = {
+1
View File
@@ -2522,6 +2522,7 @@
"view_more": "View more",
"view_only_access": "View-only access",
"view_only_downgraded": "View only. Upgrade to restore edit access.",
"view_only_reviewer_downgraded": "View only. Upgrade to restore review access.",
"view_options": "View options",
"view_pdf": "View PDF",
"view_source": "View Source",
@@ -992,6 +992,10 @@ class User {
updateOp = {
$addToSet: { readOnly_refs: user._id, pendingEditor_refs: user._id },
}
} else if (privileges === 'pendingReviewer') {
updateOp = {
$addToSet: { readOnly_refs: user._id, pendingReviewer_refs: user._id },
}
} else if (privileges === 'review') {
updateOp = {
$addToSet: { reviewer_refs: user._id },
@@ -18,6 +18,7 @@ describe('CollaboratorsGetter', function () {
this.readOnlyRef1 = new ObjectId()
this.readOnlyRef2 = new ObjectId()
this.pendingEditorRef = new ObjectId()
this.pendingReviewerRef = new ObjectId()
this.readWriteRef1 = new ObjectId()
this.readWriteRef2 = new ObjectId()
this.reviewer1Ref = new ObjectId()
@@ -32,8 +33,10 @@ describe('CollaboratorsGetter', function () {
this.readOnlyRef1,
this.readOnlyRef2,
this.pendingEditorRef,
this.pendingReviewerRef,
],
pendingEditor_refs: [this.pendingEditorRef],
pendingReviewer_refs: [this.pendingReviewerRef],
collaberator_refs: [this.readWriteRef1, this.readWriteRef2],
reviewer_refs: [this.reviewer1Ref, this.reviewer2Ref],
tokenAccessReadAndWrite_refs: [this.readWriteTokenRef],
@@ -114,6 +117,12 @@ describe('CollaboratorsGetter', function () {
source: 'invite',
pendingEditor: true,
},
{
id: this.pendingReviewerRef.toString(),
privilegeLevel: 'readOnly',
source: 'invite',
pendingReviewer: true,
},
{
id: this.readOnlyTokenRef.toString(),
privilegeLevel: 'readOnly',
@@ -165,6 +174,7 @@ describe('CollaboratorsGetter', function () {
this.readWriteRef1.toString(),
this.readWriteRef2.toString(),
this.pendingEditorRef.toString(),
this.pendingReviewerRef.toString(),
this.readWriteTokenRef.toString(),
this.readOnlyTokenRef.toString(),
this.reviewer1Ref.toString(),
@@ -186,6 +196,7 @@ describe('CollaboratorsGetter', function () {
this.readWriteRef1.toString(),
this.readWriteRef2.toString(),
this.pendingEditorRef.toString(),
this.pendingReviewerRef.toString(),
this.reviewer1Ref.toString(),
this.reviewer2Ref.toString(),
])
@@ -545,12 +556,12 @@ describe('CollaboratorsGetter', function () {
})
describe('getInvitedPendingEditorCount', function () {
it('should return the count of pending editors', async function () {
it('should return the count of pending editors and reviewers', async function () {
const count =
await this.CollaboratorsGetter.promises.getInvitedPendingEditorCount(
this.project._id
)
expect(count).to.equal(1)
expect(count).to.equal(2)
})
})
})
@@ -111,6 +111,7 @@ describe('CollaboratorsHandler', function () {
reviewer_refs: this.userId,
readOnly_refs: this.userId,
pendingEditor_refs: this.userId,
pendingReviewer_refs: this.userId,
tokenAccessReadOnly_refs: this.userId,
tokenAccessReadAndWrite_refs: this.userId,
archived: this.userId,
@@ -155,6 +156,7 @@ describe('CollaboratorsHandler', function () {
reviewer_refs: this.userId,
readOnly_refs: this.userId,
pendingEditor_refs: this.userId,
pendingReviewer_refs: this.userId,
tokenAccessReadOnly_refs: this.userId,
tokenAccessReadAndWrite_refs: this.userId,
trashed: this.userId,
@@ -191,6 +193,7 @@ describe('CollaboratorsHandler', function () {
reviewer_refs: this.userId,
readOnly_refs: this.userId,
pendingEditor_refs: this.userId,
pendingReviewer_refs: this.userId,
tokenAccessReadOnly_refs: this.userId,
tokenAccessReadAndWrite_refs: this.userId,
archived: this.userId,
@@ -278,6 +281,32 @@ describe('CollaboratorsHandler', function () {
)
})
})
describe('with pendingReviewer flag', function () {
it('should add them to the pending reviewer refs', async function () {
this.ProjectMock.expects('updateOne')
.withArgs(
{
_id: this.project._id,
},
{
$addToSet: {
readOnly_refs: this.userId,
pendingReviewer_refs: this.userId,
},
}
)
.chain('exec')
.resolves()
await this.CollaboratorsHandler.promises.addUserIdToProject(
this.project._id,
this.addingUserId,
this.userId,
'readOnly',
{ pendingReviewer: true }
)
})
})
})
describe('as readAndWrite', function () {
@@ -451,6 +480,7 @@ describe('CollaboratorsHandler', function () {
reviewer_refs: this.userId,
readOnly_refs: this.userId,
pendingEditor_refs: this.userId,
pendingReviewer_refs: this.userId,
tokenAccessReadOnly_refs: this.userId,
tokenAccessReadAndWrite_refs: this.userId,
archived: this.userId,
@@ -549,6 +579,24 @@ describe('CollaboratorsHandler', function () {
)
.chain('exec')
.resolves()
this.ProjectMock.expects('updateMany')
.withArgs(
{ pendingReviewer_refs: this.fromUserId },
{
$addToSet: { pendingReviewer_refs: this.toUserId },
}
)
.chain('exec')
.resolves()
this.ProjectMock.expects('updateMany')
.withArgs(
{ pendingReviewer_refs: this.fromUserId },
{
$pull: { pendingReviewer_refs: this.fromUserId },
}
)
.chain('exec')
.resolves()
})
describe('successfully', function () {
@@ -586,7 +634,7 @@ describe('CollaboratorsHandler', function () {
this.ProjectMock.expects('updateOne')
.withArgs(
{
_id: this.projectId,
_id: this.project._id,
$or: [
{ collaberator_refs: this.userId },
{ readOnly_refs: this.userId },
@@ -597,6 +645,7 @@ describe('CollaboratorsHandler', function () {
$pull: {
collaberator_refs: this.userId,
pendingEditor_refs: this.userId,
pendingReviewer_refs: this.userId,
reviewer_refs: this.userId,
},
$addToSet: { readOnly_refs: this.userId },
@@ -605,7 +654,7 @@ describe('CollaboratorsHandler', function () {
.chain('exec')
.resolves({ matchedCount: 1 })
await this.CollaboratorsHandler.promises.setCollaboratorPrivilegeLevel(
this.projectId,
this.project._id,
this.userId,
'readOnly'
)
@@ -615,7 +664,7 @@ describe('CollaboratorsHandler', function () {
this.ProjectMock.expects('updateOne')
.withArgs(
{
_id: this.projectId,
_id: this.project._id,
$or: [
{ collaberator_refs: this.userId },
{ readOnly_refs: this.userId },
@@ -628,13 +677,14 @@ describe('CollaboratorsHandler', function () {
readOnly_refs: this.userId,
reviewer_refs: this.userId,
pendingEditor_refs: this.userId,
pendingReviewer_refs: this.userId,
},
}
)
.chain('exec')
.resolves({ matchedCount: 1 })
await this.CollaboratorsHandler.promises.setCollaboratorPrivilegeLevel(
this.projectId,
this.project._id,
this.userId,
'readAndWrite'
)
@@ -653,7 +703,7 @@ describe('CollaboratorsHandler', function () {
this.ProjectMock.expects('updateOne')
.withArgs(
{
_id: this.projectId,
_id: this.project._id,
$or: [
{ collaberator_refs: this.userId },
{ readOnly_refs: this.userId },
@@ -667,13 +717,14 @@ describe('CollaboratorsHandler', function () {
readOnly_refs: this.userId,
collaberator_refs: this.userId,
pendingEditor_refs: this.userId,
pendingReviewer_refs: this.userId,
},
}
)
.chain('exec')
.resolves({ matchedCount: 1 })
await this.CollaboratorsHandler.promises.setCollaboratorPrivilegeLevel(
this.projectId,
this.project._id,
this.userId,
'review'
)
@@ -695,7 +746,7 @@ describe('CollaboratorsHandler', function () {
this.ProjectMock.expects('updateOne')
.withArgs(
{
_id: this.projectId,
_id: this.project._id,
$or: [
{ collaberator_refs: this.userId },
{ readOnly_refs: this.userId },
@@ -709,13 +760,14 @@ describe('CollaboratorsHandler', function () {
readOnly_refs: this.userId,
collaberator_refs: this.userId,
pendingEditor_refs: this.userId,
pendingReviewer_refs: this.userId,
},
}
)
.chain('exec')
.resolves({ matchedCount: 1 })
await this.CollaboratorsHandler.promises.setCollaboratorPrivilegeLevel(
this.projectId,
this.project._id,
this.userId,
'review'
)
@@ -726,7 +778,7 @@ describe('CollaboratorsHandler', function () {
this.ProjectMock.expects('updateOne')
.withArgs(
{
_id: this.projectId,
_id: this.project._id,
$or: [
{ collaberator_refs: this.userId },
{ readOnly_refs: this.userId },
@@ -741,26 +793,60 @@ describe('CollaboratorsHandler', function () {
$pull: {
collaberator_refs: this.userId,
reviewer_refs: this.userId,
pendingReviewer_refs: this.userId,
},
}
)
.chain('exec')
.resolves({ matchedCount: 1 })
await this.CollaboratorsHandler.promises.setCollaboratorPrivilegeLevel(
this.projectId,
this.project._id,
this.userId,
'readOnly',
{ pendingEditor: true }
)
})
it('sets a collaborator to read-only as a pendingReviewer', async function () {
this.ProjectMock.expects('updateOne')
.withArgs(
{
_id: this.project._id,
$or: [
{ collaberator_refs: this.userId },
{ readOnly_refs: this.userId },
{ reviewer_refs: this.userId },
],
},
{
$addToSet: {
readOnly_refs: this.userId,
pendingReviewer_refs: this.userId,
},
$pull: {
collaberator_refs: this.userId,
reviewer_refs: this.userId,
pendingEditor_refs: this.userId,
},
}
)
.chain('exec')
.resolves({ matchedCount: 1 })
await this.CollaboratorsHandler.promises.setCollaboratorPrivilegeLevel(
this.project._id,
this.userId,
'readOnly',
{ pendingReviewer: true }
)
})
it('throws a NotFoundError if the project or collaborator does not exist', async function () {
this.ProjectMock.expects('updateOne')
.chain('exec')
.resolves({ matchedCount: 0 })
await expect(
this.CollaboratorsHandler.promises.setCollaboratorPrivilegeLevel(
this.projectId,
this.project._id,
this.userId,
'readAndWrite'
)
@@ -561,7 +561,7 @@ describe('CollaboratorsInviteHandler', function () {
'editor-moved-to-pending',
null,
null,
{ userId: this.userId.toString() }
{ userId: this.userId.toString(), role: 'editor' }
)
this.CollaboratorsHandler.promises.addUserIdToProject.should.have.been.calledWith(
this.projectId,