mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-05-30 12:24:25 +02:00
Fix transfer ownership permissions GitOrigin-RevId: b6d09704361507085e3eae8dc9240a36ae47c70e
682 lines
21 KiB
JavaScript
682 lines
21 KiB
JavaScript
import { vi, expect } from 'vitest'
|
|
import sinon from 'sinon'
|
|
import PrivilegeLevels from '../../../../app/src/Features/Authorization/PrivilegeLevels.mjs'
|
|
import Errors from '../../../../app/src/Features/Errors/Errors.js'
|
|
import mongodb from 'mongodb-legacy'
|
|
|
|
const { ObjectId } = mongodb
|
|
|
|
const MODULE_PATH =
|
|
'../../../../app/src/Features/Collaborators/OwnershipTransferHandler.mjs'
|
|
|
|
describe('OwnershipTransferHandler', function () {
|
|
beforeEach(async function (ctx) {
|
|
ctx.user = { _id: new ObjectId(), email: 'owner@example.com' }
|
|
ctx.collaborator = {
|
|
_id: new ObjectId(),
|
|
email: 'collaborator@example.com',
|
|
}
|
|
ctx.readOnlyCollaborator = {
|
|
_id: new ObjectId(),
|
|
email: 'readonly@example.com',
|
|
}
|
|
ctx.reviewer = {
|
|
_id: new ObjectId(),
|
|
email: 'reviewer@example.com',
|
|
}
|
|
ctx.project = {
|
|
_id: new ObjectId(),
|
|
name: 'project',
|
|
owner_ref: ctx.user._id,
|
|
collaberator_refs: [ctx.collaborator._id],
|
|
readOnly_refs: [ctx.readOnlyCollaborator._id],
|
|
reviewer_refs: [ctx.reviewer._id],
|
|
}
|
|
ctx.ProjectGetter = {
|
|
promises: {
|
|
getProject: sinon.stub().resolves(ctx.project),
|
|
},
|
|
}
|
|
ctx.ProjectModel = {
|
|
find: sinon.stub().resolves([]),
|
|
updateOne: sinon.stub().returns({
|
|
exec: sinon.stub().resolves(),
|
|
}),
|
|
}
|
|
ctx.UserGetter = {
|
|
promises: {
|
|
getUser: sinon.stub().resolves(),
|
|
},
|
|
}
|
|
ctx.TpdsUpdateSender = {
|
|
promises: {
|
|
moveEntity: sinon.stub().resolves(),
|
|
},
|
|
}
|
|
ctx.TpdsProjectFlusher = {
|
|
promises: {
|
|
flushProjectToTpds: sinon.stub().resolves(),
|
|
},
|
|
}
|
|
ctx.CollaboratorsHandler = {
|
|
promises: {
|
|
removeUserFromProject: sinon.stub().resolves(),
|
|
addUserIdToProject: sinon.stub().resolves(),
|
|
},
|
|
}
|
|
ctx.EmailHandler = {
|
|
promises: {
|
|
sendEmail: sinon.stub().resolves(),
|
|
},
|
|
}
|
|
ctx.ProjectAuditLogHandler = {
|
|
promises: {
|
|
addEntry: sinon.stub().resolves(),
|
|
},
|
|
}
|
|
ctx.TagsHandler = {
|
|
promises: {
|
|
createTag: sinon.stub().resolves(),
|
|
addProjectsToTag: sinon.stub().resolves(),
|
|
},
|
|
}
|
|
ctx.LimitationsManager = {
|
|
promises: {
|
|
canAddXEditCollaborators: sinon.stub().resolves(true),
|
|
},
|
|
}
|
|
|
|
vi.mock('../../../../app/src/Features/Errors/Errors.js', () =>
|
|
vi.importActual('../../../../app/src/Features/Errors/Errors.js')
|
|
)
|
|
|
|
vi.doMock('../../../../app/src/Features/Project/ProjectGetter.mjs', () => ({
|
|
default: ctx.ProjectGetter,
|
|
}))
|
|
|
|
vi.doMock('../../../../app/src/models/Project.mjs', () => ({
|
|
Project: ctx.ProjectModel,
|
|
}))
|
|
|
|
vi.doMock('../../../../app/src/Features/Tags/TagsHandler.mjs', () => ({
|
|
default: ctx.TagsHandler,
|
|
}))
|
|
|
|
vi.doMock('../../../../app/src/Features/User/UserGetter.mjs', () => ({
|
|
default: ctx.UserGetter,
|
|
}))
|
|
|
|
vi.doMock(
|
|
'../../../../app/src/Features/ThirdPartyDataStore/TpdsProjectFlusher.mjs',
|
|
() => ({
|
|
default: ctx.TpdsProjectFlusher,
|
|
})
|
|
)
|
|
|
|
vi.doMock(
|
|
'../../../../app/src/Features/Project/ProjectAuditLogHandler.mjs',
|
|
() => ({
|
|
default: ctx.ProjectAuditLogHandler,
|
|
})
|
|
)
|
|
|
|
vi.doMock('../../../../app/src/Features/Email/EmailHandler.mjs', () => ({
|
|
default: ctx.EmailHandler,
|
|
}))
|
|
|
|
vi.doMock(
|
|
'../../../../app/src/Features/Collaborators/CollaboratorsHandler.mjs',
|
|
() => ({
|
|
default: ctx.CollaboratorsHandler,
|
|
})
|
|
)
|
|
|
|
vi.doMock(
|
|
'../../../../app/src/Features/Analytics/AnalyticsManager',
|
|
() => ({
|
|
default: {
|
|
recordEventForUserInBackground: (ctx.recordEventForUserInBackground =
|
|
sinon.stub()),
|
|
},
|
|
})
|
|
)
|
|
|
|
vi.doMock(
|
|
'../../../../app/src/Features/Subscription/LimitationsManager.mjs',
|
|
() => ({
|
|
default: ctx.LimitationsManager,
|
|
})
|
|
)
|
|
|
|
ctx.handler = (await import(MODULE_PATH)).default
|
|
})
|
|
|
|
describe('transferOwnership', function () {
|
|
beforeEach(function (ctx) {
|
|
ctx.UserGetter.promises.getUser.withArgs(ctx.user._id).resolves(ctx.user)
|
|
ctx.UserGetter.promises.getUser
|
|
.withArgs(ctx.collaborator._id)
|
|
.resolves(ctx.collaborator)
|
|
ctx.UserGetter.promises.getUser
|
|
.withArgs(ctx.readOnlyCollaborator._id)
|
|
.resolves(ctx.readOnlyCollaborator)
|
|
ctx.UserGetter.promises.getUser
|
|
.withArgs(ctx.reviewer._id)
|
|
.resolves(ctx.reviewer)
|
|
})
|
|
|
|
it("should return a not found error if the project can't be found", async function (ctx) {
|
|
ctx.ProjectGetter.promises.getProject.resolves(null)
|
|
await expect(
|
|
ctx.handler.promises.transferOwnership('abc', ctx.collaborator._id)
|
|
).to.be.rejectedWith(Errors.ProjectNotFoundError)
|
|
})
|
|
|
|
it("should return a not found error if the user can't be found", async function (ctx) {
|
|
ctx.UserGetter.promises.getUser
|
|
.withArgs(ctx.collaborator._id)
|
|
.resolves(null)
|
|
await expect(
|
|
ctx.handler.promises.transferOwnership(
|
|
ctx.project._id,
|
|
ctx.collaborator._id
|
|
)
|
|
).to.be.rejectedWith(Errors.UserNotFoundError)
|
|
})
|
|
|
|
it('should return an error if user cannot be removed as collaborator ', async function (ctx) {
|
|
ctx.CollaboratorsHandler.promises.removeUserFromProject.rejects(
|
|
new Error('user-cannot-be-removed')
|
|
)
|
|
await expect(
|
|
ctx.handler.promises.transferOwnership(
|
|
ctx.project._id,
|
|
ctx.collaborator._id
|
|
)
|
|
).to.be.rejected
|
|
})
|
|
|
|
it('should transfer ownership of the project', async function (ctx) {
|
|
await ctx.handler.promises.transferOwnership(
|
|
ctx.project._id,
|
|
ctx.collaborator._id
|
|
)
|
|
expect(ctx.ProjectModel.updateOne).to.have.been.calledWith(
|
|
{ _id: ctx.project._id },
|
|
sinon.match({ $set: { owner_ref: ctx.collaborator._id } })
|
|
)
|
|
})
|
|
|
|
it('should check collaborator limits after ownership transfer', async function (ctx) {
|
|
await ctx.handler.promises.transferOwnership(
|
|
ctx.project._id,
|
|
ctx.collaborator._id
|
|
)
|
|
|
|
expect(ctx.ProjectModel.updateOne).to.have.been.calledBefore(
|
|
ctx.LimitationsManager.promises.canAddXEditCollaborators
|
|
)
|
|
|
|
expect(
|
|
ctx.LimitationsManager.promises.canAddXEditCollaborators
|
|
).to.have.been.calledBefore(
|
|
ctx.CollaboratorsHandler.promises.addUserIdToProject
|
|
)
|
|
})
|
|
|
|
describe('when there are edit collaborator slots available', function () {
|
|
beforeEach(function (ctx) {
|
|
ctx.LimitationsManager.promises.canAddXEditCollaborators.resolves(true)
|
|
})
|
|
|
|
it('should give old owner read/write permissions when transferring to a read-only collaborator', async function (ctx) {
|
|
await ctx.handler.promises.transferOwnership(
|
|
ctx.project._id,
|
|
ctx.readOnlyCollaborator._id
|
|
)
|
|
expect(
|
|
ctx.CollaboratorsHandler.promises.addUserIdToProject
|
|
).to.have.been.calledWith(
|
|
ctx.project._id,
|
|
ctx.readOnlyCollaborator._id,
|
|
ctx.user._id,
|
|
PrivilegeLevels.READ_AND_WRITE,
|
|
undefined
|
|
)
|
|
expect(
|
|
ctx.LimitationsManager.promises.canAddXEditCollaborators
|
|
).to.have.been.calledWith(ctx.project._id, 1)
|
|
})
|
|
|
|
it('should give old owner read/write permissions when transferring to an editor', async function (ctx) {
|
|
await ctx.handler.promises.transferOwnership(
|
|
ctx.project._id,
|
|
ctx.collaborator._id
|
|
)
|
|
expect(
|
|
ctx.CollaboratorsHandler.promises.addUserIdToProject
|
|
).to.have.been.calledWith(
|
|
ctx.project._id,
|
|
ctx.collaborator._id,
|
|
ctx.user._id,
|
|
PrivilegeLevels.READ_AND_WRITE,
|
|
undefined
|
|
)
|
|
expect(
|
|
ctx.LimitationsManager.promises.canAddXEditCollaborators
|
|
).to.have.been.calledWith(ctx.project._id, 1)
|
|
})
|
|
|
|
it('should give old owner read/write permissions when transferring to a reviewer', async function (ctx) {
|
|
await ctx.handler.promises.transferOwnership(
|
|
ctx.project._id,
|
|
ctx.reviewer._id
|
|
)
|
|
expect(
|
|
ctx.CollaboratorsHandler.promises.addUserIdToProject
|
|
).to.have.been.calledWith(
|
|
ctx.project._id,
|
|
ctx.reviewer._id,
|
|
ctx.user._id,
|
|
PrivilegeLevels.READ_AND_WRITE,
|
|
undefined
|
|
)
|
|
expect(
|
|
ctx.LimitationsManager.promises.canAddXEditCollaborators
|
|
).to.have.been.calledWith(ctx.project._id, 1)
|
|
})
|
|
|
|
it('should give old owner read/write permissions when transferring to a non-collaborator', async function (ctx) {
|
|
ctx.project.collaberator_refs = []
|
|
ctx.project.readOnly_refs = []
|
|
ctx.project.reviewer_refs = []
|
|
const newOwner = {
|
|
_id: new ObjectId(),
|
|
email: 'admin@example.com',
|
|
}
|
|
ctx.UserGetter.promises.getUser
|
|
.withArgs(newOwner._id)
|
|
.resolves(newOwner)
|
|
|
|
await ctx.handler.promises.transferOwnership(
|
|
ctx.project._id,
|
|
newOwner._id,
|
|
{ allowTransferToNonCollaborators: true }
|
|
)
|
|
expect(
|
|
ctx.CollaboratorsHandler.promises.addUserIdToProject
|
|
).to.have.been.calledWith(
|
|
ctx.project._id,
|
|
newOwner._id,
|
|
ctx.user._id,
|
|
PrivilegeLevels.READ_AND_WRITE,
|
|
undefined
|
|
)
|
|
expect(
|
|
ctx.LimitationsManager.promises.canAddXEditCollaborators
|
|
).to.have.been.calledWith(ctx.project._id, 1)
|
|
})
|
|
})
|
|
|
|
describe('when there are no edit collaborator slots available', function () {
|
|
beforeEach(function (ctx) {
|
|
ctx.LimitationsManager.promises.canAddXEditCollaborators.resolves(false)
|
|
})
|
|
|
|
it('should give old owner read-only with pending editor flag when transferring to an editor', async function (ctx) {
|
|
await ctx.handler.promises.transferOwnership(
|
|
ctx.project._id,
|
|
ctx.collaborator._id
|
|
)
|
|
expect(
|
|
ctx.CollaboratorsHandler.promises.addUserIdToProject
|
|
).to.have.been.calledWith(
|
|
ctx.project._id,
|
|
ctx.collaborator._id,
|
|
ctx.user._id,
|
|
PrivilegeLevels.READ_ONLY,
|
|
{ pendingEditor: true }
|
|
)
|
|
})
|
|
|
|
it('should give old owner read-only with pending editor flag when transferring to a reviewer', async function (ctx) {
|
|
await ctx.handler.promises.transferOwnership(
|
|
ctx.project._id,
|
|
ctx.reviewer._id
|
|
)
|
|
expect(
|
|
ctx.CollaboratorsHandler.promises.addUserIdToProject
|
|
).to.have.been.calledWith(
|
|
ctx.project._id,
|
|
ctx.reviewer._id,
|
|
ctx.user._id,
|
|
PrivilegeLevels.READ_ONLY,
|
|
{ pendingEditor: true }
|
|
)
|
|
})
|
|
|
|
it('should give old owner read-only with pending editor flag when transferring to a read-only collaborator', async function (ctx) {
|
|
await ctx.handler.promises.transferOwnership(
|
|
ctx.project._id,
|
|
ctx.readOnlyCollaborator._id
|
|
)
|
|
expect(
|
|
ctx.CollaboratorsHandler.promises.addUserIdToProject
|
|
).to.have.been.calledWith(
|
|
ctx.project._id,
|
|
ctx.readOnlyCollaborator._id,
|
|
ctx.user._id,
|
|
PrivilegeLevels.READ_ONLY,
|
|
{ pendingEditor: true }
|
|
)
|
|
})
|
|
|
|
it('should give old owner read-only with pending editor flag when transferring to a non-collaborator', async function (ctx) {
|
|
ctx.project.collaberator_refs = []
|
|
ctx.project.readOnly_refs = []
|
|
ctx.project.reviewer_refs = []
|
|
const newOwner = {
|
|
_id: new ObjectId(),
|
|
email: 'newowner@example.com',
|
|
}
|
|
ctx.UserGetter.promises.getUser
|
|
.withArgs(newOwner._id)
|
|
.resolves(newOwner)
|
|
|
|
await ctx.handler.promises.transferOwnership(
|
|
ctx.project._id,
|
|
newOwner._id,
|
|
{ allowTransferToNonCollaborators: true }
|
|
)
|
|
expect(
|
|
ctx.CollaboratorsHandler.promises.addUserIdToProject
|
|
).to.have.been.calledWith(
|
|
ctx.project._id,
|
|
newOwner._id,
|
|
ctx.user._id,
|
|
PrivilegeLevels.READ_ONLY,
|
|
{ pendingEditor: true }
|
|
)
|
|
})
|
|
})
|
|
|
|
it('should do nothing if transferring back to the owner', async function (ctx) {
|
|
await ctx.handler.promises.transferOwnership(
|
|
ctx.project._id,
|
|
ctx.user._id
|
|
)
|
|
expect(ctx.ProjectModel.updateOne).not.to.have.been.called
|
|
})
|
|
|
|
it("should remove the user from the project's collaborators", async function (ctx) {
|
|
await ctx.handler.promises.transferOwnership(
|
|
ctx.project._id,
|
|
ctx.collaborator._id
|
|
)
|
|
expect(
|
|
ctx.CollaboratorsHandler.promises.removeUserFromProject
|
|
).to.have.been.calledWith(ctx.project._id, ctx.collaborator._id)
|
|
})
|
|
|
|
it('should transfer ownership of the project to a reviewer', async function (ctx) {
|
|
await ctx.handler.promises.transferOwnership(
|
|
ctx.project._id,
|
|
ctx.reviewer._id
|
|
)
|
|
expect(ctx.ProjectModel.updateOne).to.have.been.calledWith(
|
|
{ _id: ctx.project._id },
|
|
sinon.match({ $set: { owner_ref: ctx.reviewer._id } })
|
|
)
|
|
})
|
|
|
|
it('should flush the project to tpds', async function (ctx) {
|
|
await ctx.handler.promises.transferOwnership(
|
|
ctx.project._id,
|
|
ctx.collaborator._id
|
|
)
|
|
expect(
|
|
ctx.TpdsProjectFlusher.promises.flushProjectToTpds
|
|
).to.have.been.calledWith(ctx.project._id)
|
|
})
|
|
|
|
it('should send an email notification', async function (ctx) {
|
|
await ctx.handler.promises.transferOwnership(
|
|
ctx.project._id,
|
|
ctx.collaborator._id
|
|
)
|
|
expect(ctx.EmailHandler.promises.sendEmail).to.have.been.calledWith(
|
|
'ownershipTransferConfirmationPreviousOwner',
|
|
{
|
|
to: ctx.user.email,
|
|
project: ctx.project,
|
|
newOwner: ctx.collaborator,
|
|
}
|
|
)
|
|
expect(ctx.EmailHandler.promises.sendEmail).to.have.been.calledWith(
|
|
'ownershipTransferConfirmationNewOwner',
|
|
{
|
|
to: ctx.collaborator.email,
|
|
project: ctx.project,
|
|
previousOwner: ctx.user,
|
|
}
|
|
)
|
|
})
|
|
|
|
it('should not send an email notification with the skipEmails option', async function (ctx) {
|
|
await ctx.handler.promises.transferOwnership(
|
|
ctx.project._id,
|
|
ctx.collaborator._id,
|
|
{ skipEmails: true }
|
|
)
|
|
expect(ctx.EmailHandler.promises.sendEmail).not.to.have.been.called
|
|
})
|
|
|
|
it('should track the change in BigQuery', async function (ctx) {
|
|
const sessionUserId = new ObjectId()
|
|
await ctx.handler.promises.transferOwnership(
|
|
ctx.project._id,
|
|
ctx.collaborator._id,
|
|
{ sessionUserId }
|
|
)
|
|
expect(ctx.recordEventForUserInBackground).to.have.been.calledWith(
|
|
ctx.user._id,
|
|
'project-ownership-transfer',
|
|
{
|
|
projectId: ctx.project._id,
|
|
newOwnerId: ctx.collaborator._id,
|
|
}
|
|
)
|
|
})
|
|
|
|
it('should write an entry in the audit log', async function (ctx) {
|
|
const sessionUserId = new ObjectId()
|
|
const ipAddress = '1.2.3.4'
|
|
await ctx.handler.promises.transferOwnership(
|
|
ctx.project._id,
|
|
ctx.collaborator._id,
|
|
{ sessionUserId, ipAddress }
|
|
)
|
|
expect(
|
|
ctx.ProjectAuditLogHandler.promises.addEntry
|
|
).to.have.been.calledWith(
|
|
ctx.project._id,
|
|
'transfer-ownership',
|
|
sessionUserId,
|
|
ipAddress,
|
|
{
|
|
previousOwnerId: ctx.user._id,
|
|
newOwnerId: ctx.collaborator._id,
|
|
}
|
|
)
|
|
})
|
|
|
|
it('should decline to transfer ownership to a non-collaborator', async function (ctx) {
|
|
ctx.project.collaberator_refs = []
|
|
ctx.project.readOnly_refs = []
|
|
await expect(
|
|
ctx.handler.promises.transferOwnership(
|
|
ctx.project._id,
|
|
ctx.collaborator._id
|
|
)
|
|
).to.be.rejectedWith(Errors.UserNotCollaboratorError)
|
|
})
|
|
})
|
|
|
|
describe('transferAllProjectsToUser', function () {
|
|
const fromUserEmail = 'user.one@example.com'
|
|
const ipAddress = '1.2.3.4'
|
|
let fromUserId, toUserId
|
|
beforeEach(function () {
|
|
fromUserId = new ObjectId().toString()
|
|
toUserId = new ObjectId().toString()
|
|
})
|
|
|
|
describe('with missing user', function () {
|
|
it('should throw an error', async function (ctx) {
|
|
ctx.UserGetter.promises.getUser.withArgs(fromUserId).resolves(null)
|
|
ctx.UserGetter.promises.getUser
|
|
.withArgs(toUserId)
|
|
.resolves({ _id: new ObjectId(toUserId) })
|
|
await expect(
|
|
ctx.handler.promises.transferAllProjectsToUser({
|
|
toUserId,
|
|
fromUserId,
|
|
ipAddress,
|
|
})
|
|
).to.be.rejectedWith(/missing source user/)
|
|
|
|
ctx.UserGetter.promises.getUser
|
|
.withArgs(fromUserId)
|
|
.resolves({ _id: new ObjectId(fromUserId), email: fromUserEmail })
|
|
ctx.UserGetter.promises.getUser.withArgs(toUserId).resolves(null)
|
|
await expect(
|
|
ctx.handler.promises.transferAllProjectsToUser({
|
|
fromUserId,
|
|
toUserId,
|
|
ipAddress,
|
|
})
|
|
).to.be.rejectedWith(/missing destination user/)
|
|
})
|
|
})
|
|
|
|
describe('with the same id', function () {
|
|
it('should throw an error', async function (ctx) {
|
|
ctx.UserGetter.promises.getUser
|
|
.withArgs(fromUserId)
|
|
.resolves({ _id: new ObjectId(fromUserId), email: fromUserEmail })
|
|
await expect(
|
|
ctx.handler.promises.transferAllProjectsToUser({
|
|
fromUserId,
|
|
toUserId: fromUserId,
|
|
ipAddress,
|
|
})
|
|
).to.be.rejectedWith(/rejecting transfer between identical users/)
|
|
})
|
|
})
|
|
|
|
describe('happy path', function () {
|
|
let tag, fromUserEmail, projects
|
|
|
|
beforeEach(function (ctx) {
|
|
tag = {
|
|
_id: new ObjectId(),
|
|
name: 'some-tag-name',
|
|
}
|
|
projects = [
|
|
{ _id: 'project-1' },
|
|
{ _id: 'project-2' },
|
|
{ _id: 'project-3' },
|
|
]
|
|
|
|
ctx.UserGetter.promises.getUser.withArgs(fromUserId).resolves({
|
|
_id: new ObjectId(fromUserId),
|
|
email: fromUserEmail,
|
|
})
|
|
ctx.UserGetter.promises.getUser.withArgs(toUserId).resolves({
|
|
_id: new ObjectId(toUserId),
|
|
})
|
|
ctx.ProjectModel.find.resolves(projects)
|
|
ctx.TagsHandler.promises.createTag.resolves({
|
|
_id: tag._id,
|
|
name: 'some-tag-name',
|
|
})
|
|
ctx.TagsHandler.promises.addProjectsToTag.resolves()
|
|
})
|
|
|
|
it('creates a tag', async function (ctx) {
|
|
await ctx.handler.promises.transferAllProjectsToUser({
|
|
fromUserId,
|
|
toUserId,
|
|
ipAddress,
|
|
})
|
|
expect(ctx.TagsHandler.promises.createTag).to.have.been.calledWith(
|
|
toUserId,
|
|
`transferred-from-${fromUserEmail}`,
|
|
'#434AF0',
|
|
{ truncate: true }
|
|
)
|
|
})
|
|
|
|
it('returns a projectCount, and tag name', async function (ctx) {
|
|
const result = await ctx.handler.promises.transferAllProjectsToUser({
|
|
fromUserId,
|
|
toUserId,
|
|
ipAddress,
|
|
})
|
|
expect(result.projectCount).to.equal(projects.length)
|
|
expect(result.newTagName).to.equal('some-tag-name')
|
|
})
|
|
|
|
it('gets the user records', async function (ctx) {
|
|
await ctx.handler.promises.transferAllProjectsToUser({
|
|
fromUserId,
|
|
toUserId,
|
|
ipAddress,
|
|
})
|
|
expect(ctx.UserGetter.promises.getUser).to.have.been.calledWith(
|
|
fromUserId
|
|
)
|
|
expect(ctx.UserGetter.promises.getUser).to.have.been.calledWith(
|
|
toUserId
|
|
)
|
|
})
|
|
|
|
it('gets the list of affected projects', async function (ctx) {
|
|
await ctx.handler.promises.transferAllProjectsToUser({
|
|
fromUserId,
|
|
toUserId,
|
|
ipAddress,
|
|
})
|
|
expect(ctx.ProjectModel.find).to.have.been.calledWith({
|
|
owner_ref: fromUserId,
|
|
})
|
|
})
|
|
|
|
it('transfers all of the projects', async function (ctx) {
|
|
await ctx.handler.promises.transferAllProjectsToUser({
|
|
fromUserId,
|
|
toUserId,
|
|
ipAddress,
|
|
})
|
|
|
|
expect(ctx.ProjectModel.updateOne.callCount).to.equal(3)
|
|
expect(ctx.TagsHandler.promises.addProjectsToTag.callCount).to.equal(1)
|
|
|
|
for (const project of projects) {
|
|
expect(ctx.ProjectModel.updateOne).to.have.been.calledWith(
|
|
{ _id: project._id },
|
|
sinon.match({ $set: { owner_ref: toUserId } })
|
|
)
|
|
}
|
|
expect(
|
|
ctx.TagsHandler.promises.addProjectsToTag
|
|
).to.have.been.calledWith(
|
|
toUserId,
|
|
tag._id,
|
|
projects.map(p => p._id)
|
|
)
|
|
})
|
|
})
|
|
})
|
|
})
|