mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
c371732e6e
[web] Use constructor for ObjectId GitOrigin-RevId: 9eb8b377ea599605b72af237d1ab12f4d8287162
619 lines
17 KiB
JavaScript
619 lines
17 KiB
JavaScript
const SandboxedModule = require('sandboxed-module')
|
|
const sinon = require('sinon')
|
|
const { expect } = require('chai')
|
|
const { ObjectId } = require('mongodb')
|
|
const Errors = require('../../../../app/src/Features/Errors/Errors')
|
|
|
|
const MODULE_PATH =
|
|
'../../../../app/src/Features/ThirdPartyDataStore/TpdsUpdateHandler.js'
|
|
|
|
describe('TpdsUpdateHandler', function () {
|
|
beforeEach(function () {
|
|
this.clock = sinon.useFakeTimers()
|
|
})
|
|
|
|
afterEach(function () {
|
|
this.clock.restore()
|
|
})
|
|
|
|
beforeEach(function () {
|
|
this.projectName = 'My recipes'
|
|
this.projects = {
|
|
active1: { _id: new ObjectId(), name: this.projectName },
|
|
active2: { _id: new ObjectId(), name: this.projectName },
|
|
archived1: {
|
|
_id: new ObjectId(),
|
|
name: this.projectName,
|
|
archived: [this.userId],
|
|
},
|
|
archived2: {
|
|
_id: new ObjectId(),
|
|
name: this.projectName,
|
|
archived: [this.userId],
|
|
},
|
|
}
|
|
this.userId = new ObjectId()
|
|
this.source = 'dropbox'
|
|
this.path = `/some/file`
|
|
this.update = {}
|
|
this.folderPath = '/some/folder'
|
|
this.folder = {
|
|
_id: new ObjectId(),
|
|
parentFolder_id: new ObjectId(),
|
|
}
|
|
|
|
this.CooldownManager = {
|
|
promises: {
|
|
isProjectOnCooldown: sinon.stub().resolves(false),
|
|
},
|
|
}
|
|
this.FileTypeManager = {
|
|
promises: {
|
|
shouldIgnore: sinon.stub().resolves(false),
|
|
},
|
|
}
|
|
this.Modules = {
|
|
promises: {
|
|
hooks: { fire: sinon.stub().resolves() },
|
|
},
|
|
}
|
|
this.notification = {
|
|
create: sinon.stub().resolves(),
|
|
}
|
|
this.NotificationsBuilder = {
|
|
promises: {
|
|
dropboxDuplicateProjectNames: sinon.stub().returns(this.notification),
|
|
},
|
|
}
|
|
this.ProjectCreationHandler = {
|
|
promises: {
|
|
createBlankProject: sinon.stub().resolves(this.projects.active1),
|
|
},
|
|
}
|
|
this.ProjectDeleter = {
|
|
promises: {
|
|
markAsDeletedByExternalSource: sinon.stub().resolves(),
|
|
},
|
|
}
|
|
this.ProjectGetter = {
|
|
promises: {
|
|
findUsersProjectsByName: sinon.stub(),
|
|
findAllUsersProjects: sinon
|
|
.stub()
|
|
.resolves({ owned: [this.projects.active1], readAndWrite: [] }),
|
|
},
|
|
}
|
|
this.ProjectHelper = {
|
|
isArchivedOrTrashed: sinon.stub().returns(false),
|
|
}
|
|
this.ProjectHelper.isArchivedOrTrashed
|
|
.withArgs(this.projects.archived1, this.userId)
|
|
.returns(true)
|
|
this.ProjectHelper.isArchivedOrTrashed
|
|
.withArgs(this.projects.archived2, this.userId)
|
|
.returns(true)
|
|
this.RootDocManager = {
|
|
promises: {
|
|
setRootDocAutomatically: sinon.stub().resolves(),
|
|
},
|
|
}
|
|
this.UpdateMerger = {
|
|
promises: {
|
|
deleteUpdate: sinon.stub().resolves(),
|
|
mergeUpdate: sinon.stub().resolves(),
|
|
createFolder: sinon.stub().resolves(this.folder),
|
|
},
|
|
}
|
|
|
|
this.TpdsUpdateHandler = SandboxedModule.require(MODULE_PATH, {
|
|
requires: {
|
|
'../Cooldown/CooldownManager': this.CooldownManager,
|
|
'../Uploads/FileTypeManager': this.FileTypeManager,
|
|
'../../infrastructure/Modules': this.Modules,
|
|
'../Notifications/NotificationsBuilder': this.NotificationsBuilder,
|
|
'../Project/ProjectCreationHandler': this.ProjectCreationHandler,
|
|
'../Project/ProjectDeleter': this.ProjectDeleter,
|
|
'../Project/ProjectGetter': this.ProjectGetter,
|
|
'../Project/ProjectHelper': this.ProjectHelper,
|
|
'../Project/ProjectRootDocManager': this.RootDocManager,
|
|
'./UpdateMerger': this.UpdateMerger,
|
|
},
|
|
})
|
|
})
|
|
|
|
describe('getting an update', function () {
|
|
describe('byId', function () {
|
|
describe('with no matching project', function () {
|
|
beforeEach(function () {
|
|
this.projectId = new ObjectId().toString()
|
|
})
|
|
receiveUpdateById()
|
|
expectProjectNotCreated()
|
|
expectUpdateNotProcessed()
|
|
})
|
|
|
|
describe('with one matching active project', function () {
|
|
beforeEach(function () {
|
|
this.projectId = this.projects.active1._id.toString()
|
|
})
|
|
receiveUpdateById()
|
|
expectProjectNotCreated()
|
|
expectUpdateProcessed()
|
|
})
|
|
})
|
|
|
|
describe('with no matching project', function () {
|
|
setupMatchingProjects([])
|
|
receiveUpdate()
|
|
expectProjectCreated()
|
|
expectUpdateProcessed()
|
|
})
|
|
|
|
describe('with one matching active project', function () {
|
|
setupMatchingProjects(['active1'])
|
|
receiveUpdate()
|
|
expectProjectNotCreated()
|
|
expectUpdateProcessed()
|
|
})
|
|
|
|
describe('with one matching archived project', function () {
|
|
setupMatchingProjects(['archived1'])
|
|
receiveUpdate()
|
|
expectProjectNotCreated()
|
|
expectUpdateNotProcessed()
|
|
expectDropboxNotUnlinked()
|
|
})
|
|
|
|
describe('with two matching active projects', function () {
|
|
setupMatchingProjects(['active1', 'active2'])
|
|
receiveUpdate()
|
|
expectProjectNotCreated()
|
|
expectUpdateNotProcessed()
|
|
expectDropboxUnlinked()
|
|
})
|
|
|
|
describe('with two matching archived projects', function () {
|
|
setupMatchingProjects(['archived1', 'archived2'])
|
|
receiveUpdate()
|
|
expectProjectNotCreated()
|
|
expectUpdateNotProcessed()
|
|
expectDropboxNotUnlinked()
|
|
})
|
|
|
|
describe('with one matching active and one matching archived project', function () {
|
|
setupMatchingProjects(['active1', 'archived1'])
|
|
receiveUpdate()
|
|
expectProjectNotCreated()
|
|
expectUpdateNotProcessed()
|
|
expectDropboxUnlinked()
|
|
})
|
|
|
|
describe('update to a file that should be ignored', async function () {
|
|
setupMatchingProjects(['active1'])
|
|
beforeEach(function () {
|
|
this.FileTypeManager.promises.shouldIgnore.resolves(true)
|
|
})
|
|
receiveUpdate()
|
|
expectProjectNotCreated()
|
|
expectUpdateNotProcessed()
|
|
expectDropboxNotUnlinked()
|
|
})
|
|
|
|
describe('update to a project on cooldown', async function () {
|
|
setupMatchingProjects(['active1'])
|
|
setupProjectOnCooldown()
|
|
beforeEach(async function () {
|
|
await expect(
|
|
this.TpdsUpdateHandler.promises.newUpdate(
|
|
this.userId,
|
|
'', // projectId
|
|
this.projectName,
|
|
this.path,
|
|
this.update,
|
|
this.source
|
|
)
|
|
).to.be.rejectedWith(Errors.TooManyRequestsError)
|
|
})
|
|
expectUpdateNotProcessed()
|
|
})
|
|
})
|
|
|
|
describe('getting a file delete', function () {
|
|
describe('byId', function () {
|
|
describe('with no matching project', function () {
|
|
beforeEach(function () {
|
|
this.projectId = new ObjectId().toString()
|
|
})
|
|
receiveFileDeleteById()
|
|
expectDeleteNotProcessed()
|
|
expectProjectNotDeleted()
|
|
})
|
|
|
|
describe('with one matching active project', function () {
|
|
beforeEach(function () {
|
|
this.projectId = this.projects.active1._id.toString()
|
|
})
|
|
receiveFileDeleteById()
|
|
expectDeleteProcessed()
|
|
expectProjectNotDeleted()
|
|
})
|
|
})
|
|
|
|
describe('with no matching project', function () {
|
|
setupMatchingProjects([])
|
|
receiveFileDelete()
|
|
expectDeleteNotProcessed()
|
|
expectProjectNotDeleted()
|
|
})
|
|
|
|
describe('with one matching active project', function () {
|
|
setupMatchingProjects(['active1'])
|
|
receiveFileDelete()
|
|
expectDeleteProcessed()
|
|
expectProjectNotDeleted()
|
|
})
|
|
|
|
describe('with one matching archived project', function () {
|
|
setupMatchingProjects(['archived1'])
|
|
receiveFileDelete()
|
|
expectDeleteNotProcessed()
|
|
expectProjectNotDeleted()
|
|
})
|
|
|
|
describe('with two matching active projects', function () {
|
|
setupMatchingProjects(['active1', 'active2'])
|
|
receiveFileDelete()
|
|
expectDeleteNotProcessed()
|
|
expectProjectNotDeleted()
|
|
expectDropboxUnlinked()
|
|
})
|
|
|
|
describe('with two matching archived projects', function () {
|
|
setupMatchingProjects(['archived1', 'archived2'])
|
|
receiveFileDelete()
|
|
expectDeleteNotProcessed()
|
|
expectProjectNotDeleted()
|
|
expectDropboxNotUnlinked()
|
|
})
|
|
|
|
describe('with one matching active and one matching archived project', function () {
|
|
setupMatchingProjects(['active1', 'archived1'])
|
|
receiveFileDelete()
|
|
expectDeleteNotProcessed()
|
|
expectProjectNotDeleted()
|
|
expectDropboxUnlinked()
|
|
})
|
|
})
|
|
|
|
describe('getting a project delete', function () {
|
|
describe('with no matching project', function () {
|
|
setupMatchingProjects([])
|
|
receiveProjectDelete()
|
|
expectDeleteNotProcessed()
|
|
expectProjectNotDeleted()
|
|
})
|
|
|
|
describe('with one matching active project', function () {
|
|
setupMatchingProjects(['active1'])
|
|
receiveProjectDelete()
|
|
expectDeleteNotProcessed()
|
|
expectProjectDeleted()
|
|
})
|
|
|
|
describe('with one matching archived project', function () {
|
|
setupMatchingProjects(['archived1'])
|
|
receiveProjectDelete()
|
|
expectDeleteNotProcessed()
|
|
expectProjectNotDeleted()
|
|
})
|
|
|
|
describe('with two matching active projects', function () {
|
|
setupMatchingProjects(['active1', 'active2'])
|
|
receiveProjectDelete()
|
|
expectDeleteNotProcessed()
|
|
expectProjectNotDeleted()
|
|
expectDropboxUnlinked()
|
|
})
|
|
|
|
describe('with two matching archived projects', function () {
|
|
setupMatchingProjects(['archived1', 'archived2'])
|
|
receiveProjectDelete()
|
|
expectDeleteNotProcessed()
|
|
expectProjectNotDeleted()
|
|
expectDropboxNotUnlinked()
|
|
})
|
|
|
|
describe('with one matching active and one matching archived project', function () {
|
|
setupMatchingProjects(['active1', 'archived1'])
|
|
receiveProjectDelete()
|
|
expectDeleteNotProcessed()
|
|
expectProjectNotDeleted()
|
|
expectDropboxUnlinked()
|
|
})
|
|
})
|
|
|
|
describe('getting a folder update', function () {
|
|
describe('with no matching project', function () {
|
|
setupMatchingProjects([])
|
|
receiveFolderUpdate()
|
|
expectProjectCreated()
|
|
expectFolderUpdateProcessed()
|
|
})
|
|
|
|
describe('with one matching active project', function () {
|
|
setupMatchingProjects(['active1'])
|
|
receiveFolderUpdate()
|
|
expectProjectNotCreated()
|
|
expectFolderUpdateProcessed()
|
|
})
|
|
|
|
describe('with one matching archived project', function () {
|
|
setupMatchingProjects(['archived1'])
|
|
receiveFolderUpdate()
|
|
expectProjectNotCreated()
|
|
expectFolderUpdateNotProcessed()
|
|
expectDropboxNotUnlinked()
|
|
})
|
|
|
|
describe('with two matching active projects', function () {
|
|
setupMatchingProjects(['active1', 'active2'])
|
|
receiveFolderUpdate()
|
|
expectProjectNotCreated()
|
|
expectFolderUpdateNotProcessed()
|
|
expectDropboxUnlinked()
|
|
})
|
|
|
|
describe('with two matching archived projects', function () {
|
|
setupMatchingProjects(['archived1', 'archived2'])
|
|
receiveFolderUpdate()
|
|
expectProjectNotCreated()
|
|
expectFolderUpdateNotProcessed()
|
|
expectDropboxNotUnlinked()
|
|
})
|
|
|
|
describe('with one matching active and one matching archived project', function () {
|
|
setupMatchingProjects(['active1', 'archived1'])
|
|
receiveFolderUpdate()
|
|
expectProjectNotCreated()
|
|
expectFolderUpdateNotProcessed()
|
|
expectDropboxUnlinked()
|
|
})
|
|
|
|
describe('update to a project on cooldown', async function () {
|
|
setupMatchingProjects(['active1'])
|
|
setupProjectOnCooldown()
|
|
beforeEach(async function () {
|
|
await expect(
|
|
this.TpdsUpdateHandler.promises.createFolder(
|
|
this.userId,
|
|
this.projectId,
|
|
this.projectName,
|
|
this.path
|
|
)
|
|
).to.be.rejectedWith(Errors.TooManyRequestsError)
|
|
})
|
|
expectFolderUpdateNotProcessed()
|
|
})
|
|
})
|
|
})
|
|
|
|
/* Setup helpers */
|
|
|
|
function setupMatchingProjects(projectKeys) {
|
|
beforeEach(function () {
|
|
const projects = projectKeys.map(key => this.projects[key])
|
|
this.ProjectGetter.promises.findUsersProjectsByName
|
|
.withArgs(this.userId, this.projectName)
|
|
.resolves(projects)
|
|
})
|
|
}
|
|
|
|
function setupProjectOnCooldown() {
|
|
beforeEach(function () {
|
|
this.CooldownManager.promises.isProjectOnCooldown
|
|
.withArgs(this.projects.active1._id)
|
|
.resolves(true)
|
|
})
|
|
}
|
|
|
|
/* Test helpers */
|
|
|
|
function receiveUpdate() {
|
|
beforeEach(async function () {
|
|
await this.TpdsUpdateHandler.promises.newUpdate(
|
|
this.userId,
|
|
'', // projectId
|
|
this.projectName,
|
|
this.path,
|
|
this.update,
|
|
this.source
|
|
)
|
|
})
|
|
}
|
|
|
|
function receiveUpdateById() {
|
|
beforeEach(function (done) {
|
|
this.TpdsUpdateHandler.newUpdate(
|
|
this.userId,
|
|
this.projectId,
|
|
'', // projectName
|
|
this.path,
|
|
this.update,
|
|
this.source,
|
|
done
|
|
)
|
|
})
|
|
}
|
|
|
|
function receiveFileDelete() {
|
|
beforeEach(async function () {
|
|
await this.TpdsUpdateHandler.promises.deleteUpdate(
|
|
this.userId,
|
|
'', // projectId
|
|
this.projectName,
|
|
this.path,
|
|
this.source
|
|
)
|
|
})
|
|
}
|
|
|
|
function receiveFileDeleteById() {
|
|
beforeEach(function (done) {
|
|
this.TpdsUpdateHandler.deleteUpdate(
|
|
this.userId,
|
|
this.projectId,
|
|
'', // projectName
|
|
this.path,
|
|
this.source,
|
|
done
|
|
)
|
|
})
|
|
}
|
|
|
|
function receiveProjectDelete() {
|
|
beforeEach(async function () {
|
|
await this.TpdsUpdateHandler.promises.deleteUpdate(
|
|
this.userId,
|
|
'', // projectId
|
|
this.projectName,
|
|
'/',
|
|
this.source
|
|
)
|
|
})
|
|
}
|
|
|
|
function receiveFolderUpdate() {
|
|
beforeEach(async function () {
|
|
await this.TpdsUpdateHandler.promises.createFolder(
|
|
this.userId,
|
|
this.projectId,
|
|
this.projectName,
|
|
this.folderPath
|
|
)
|
|
})
|
|
}
|
|
|
|
/* Expectations */
|
|
|
|
function expectProjectCreated() {
|
|
it('creates a project', function () {
|
|
expect(
|
|
this.ProjectCreationHandler.promises.createBlankProject
|
|
).to.have.been.calledWith(this.userId, this.projectName)
|
|
})
|
|
|
|
it('sets the root doc', function () {
|
|
// Fire pending timers
|
|
this.clock.next()
|
|
expect(
|
|
this.RootDocManager.promises.setRootDocAutomatically
|
|
).to.have.been.calledWith(this.projects.active1._id)
|
|
})
|
|
}
|
|
|
|
function expectProjectNotCreated() {
|
|
it('does not create a project', function () {
|
|
expect(this.ProjectCreationHandler.promises.createBlankProject).not.to.have
|
|
.been.called
|
|
})
|
|
|
|
it('does not set the root doc', function () {
|
|
// Fire pending timers
|
|
this.clock.next()
|
|
expect(this.RootDocManager.promises.setRootDocAutomatically).not.to.have
|
|
.been.called
|
|
})
|
|
}
|
|
|
|
function expectUpdateProcessed() {
|
|
it('processes the update', function () {
|
|
expect(this.UpdateMerger.promises.mergeUpdate).to.have.been.calledWith(
|
|
this.userId,
|
|
this.projects.active1._id,
|
|
this.path,
|
|
this.update,
|
|
this.source
|
|
)
|
|
})
|
|
}
|
|
|
|
function expectUpdateNotProcessed() {
|
|
it('does not process the update', function () {
|
|
expect(this.UpdateMerger.promises.mergeUpdate).not.to.have.been.called
|
|
})
|
|
}
|
|
|
|
function expectFolderUpdateProcessed() {
|
|
it('processes the folder update', function () {
|
|
expect(this.UpdateMerger.promises.createFolder).to.have.been.calledWith(
|
|
this.projects.active1._id,
|
|
this.folderPath
|
|
)
|
|
})
|
|
}
|
|
|
|
function expectFolderUpdateNotProcessed() {
|
|
it("doesn't process the folder update", function () {
|
|
expect(this.UpdateMerger.promises.createFolder).not.to.have.been.called
|
|
})
|
|
}
|
|
|
|
function expectDropboxUnlinked() {
|
|
it('unlinks Dropbox', function () {
|
|
expect(this.Modules.promises.hooks.fire).to.have.been.calledWith(
|
|
'removeDropbox',
|
|
this.userId,
|
|
'duplicate-projects'
|
|
)
|
|
})
|
|
|
|
it('creates a notification that dropbox was unlinked', function () {
|
|
expect(
|
|
this.NotificationsBuilder.promises.dropboxDuplicateProjectNames
|
|
).to.have.been.calledWith(this.userId)
|
|
expect(this.notification.create).to.have.been.calledWith(this.projectName)
|
|
})
|
|
}
|
|
|
|
function expectDropboxNotUnlinked() {
|
|
it('does not unlink Dropbox', function () {
|
|
expect(this.Modules.promises.hooks.fire).not.to.have.been.called
|
|
})
|
|
|
|
it('does not create a notification that dropbox was unlinked', function () {
|
|
expect(this.NotificationsBuilder.promises.dropboxDuplicateProjectNames).not
|
|
.to.have.been.called
|
|
})
|
|
}
|
|
|
|
function expectDeleteProcessed() {
|
|
it('processes the delete', function () {
|
|
expect(this.UpdateMerger.promises.deleteUpdate).to.have.been.calledWith(
|
|
this.userId,
|
|
this.projects.active1._id,
|
|
this.path,
|
|
this.source
|
|
)
|
|
})
|
|
}
|
|
|
|
function expectDeleteNotProcessed() {
|
|
it('does not process the delete', function () {
|
|
expect(this.UpdateMerger.promises.deleteUpdate).not.to.have.been.called
|
|
})
|
|
}
|
|
|
|
function expectProjectDeleted() {
|
|
it('deletes the project', function () {
|
|
expect(
|
|
this.ProjectDeleter.promises.markAsDeletedByExternalSource
|
|
).to.have.been.calledWith(this.projects.active1._id)
|
|
})
|
|
}
|
|
|
|
function expectProjectNotDeleted() {
|
|
it('does not delete the project', function () {
|
|
expect(this.ProjectDeleter.promises.markAsDeletedByExternalSource).not.to
|
|
.have.been.called
|
|
})
|
|
}
|