overleaf/services/web/test/unit/src/ThirdPartyDataStore/TpdsUpdateHandlerTests.js
Alf Eaton 1be43911b4 Merge pull request #3942 from overleaf/prettier-trailing-comma
Set Prettier's "trailingComma" setting to "es5"

GitOrigin-RevId: 9f14150511929a855b27467ad17be6ab262fe5d5
2021-04-28 02:10:01 +00:00

432 lines
12 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.CooldownManager = {
isProjectOnCooldown: sinon.stub().yields(null, false),
}
this.FileTypeManager = {
shouldIgnore: sinon.stub().yields(null, false),
}
this.Modules = {
hooks: { fire: sinon.stub().yields() },
}
this.notification = {
create: sinon.stub().yields(),
}
this.NotificationsBuilder = {
dropboxDuplicateProjectNames: sinon.stub().returns(this.notification),
}
this.ProjectCreationHandler = {
createBlankProject: sinon.stub().yields(null, this.projects.active1),
}
this.ProjectDeleter = {
markAsDeletedByExternalSource: sinon.stub().yields(),
}
this.ProjectGetter = {
findUsersProjectsByName: sinon.stub(),
}
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 = { setRootDocAutomatically: sinon.stub() }
this.UpdateMerger = {
deleteUpdate: sinon.stub().yields(),
mergeUpdate: sinon.stub().yields(),
}
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('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', function (done) {
setupMatchingProjects(['active1'])
beforeEach(function () {
this.FileTypeManager.shouldIgnore.yields(null, true)
})
receiveUpdate()
expectProjectNotCreated()
expectUpdateNotProcessed()
expectDropboxNotUnlinked()
})
describe('update to a project on cooldown', function (done) {
setupMatchingProjects(['active1'])
setupProjectOnCooldown()
beforeEach(function (done) {
this.TpdsUpdateHandler.newUpdate(
this.userId,
this.projectName,
this.path,
this.update,
this.source,
err => {
expect(err).to.be.instanceof(Errors.TooManyRequestsError)
done()
}
)
})
expectUpdateNotProcessed()
})
})
describe('getting a file delete', function () {
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()
})
})
})
/* Setup helpers */
function setupMatchingProjects(projectKeys) {
beforeEach(function () {
const projects = projectKeys.map(key => this.projects[key])
this.ProjectGetter.findUsersProjectsByName
.withArgs(this.userId, this.projectName)
.yields(null, projects)
})
}
function setupProjectOnCooldown() {
beforeEach(function () {
this.CooldownManager.isProjectOnCooldown
.withArgs(this.projects.active1._id)
.yields(null, true)
})
}
/* Test helpers */
function receiveUpdate() {
beforeEach(function (done) {
this.TpdsUpdateHandler.newUpdate(
this.userId,
this.projectName,
this.path,
this.update,
this.source,
done
)
})
}
function receiveFileDelete() {
beforeEach(function (done) {
this.TpdsUpdateHandler.deleteUpdate(
this.userId,
this.projectName,
this.path,
this.source,
done
)
})
}
function receiveProjectDelete() {
beforeEach(function (done) {
this.TpdsUpdateHandler.deleteUpdate(
this.userId,
this.projectName,
'/',
this.source,
done
)
})
}
/* Expectations */
function expectProjectCreated() {
it('creates a project', function () {
expect(
this.ProjectCreationHandler.createBlankProject
).to.have.been.calledWith(this.userId, this.projectName)
})
it('sets the root doc', function () {
// Fire pending timers
this.clock.runAll()
expect(this.RootDocManager.setRootDocAutomatically).to.have.been.calledWith(
this.projects.active1._id
)
})
}
function expectProjectNotCreated() {
it('does not create a project', function () {
expect(this.ProjectCreationHandler.createBlankProject).not.to.have.been
.called
})
it('does not set the root doc', function () {
// Fire pending timers
this.clock.runAll()
expect(this.RootDocManager.setRootDocAutomatically).not.to.have.been.called
})
}
function expectUpdateProcessed() {
it('processes the update', function () {
expect(this.UpdateMerger.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.mergeUpdate).not.to.have.been.called
})
}
function expectDropboxUnlinked() {
it('unlinks Dropbox', function () {
expect(this.Modules.hooks.fire).to.have.been.calledWith(
'removeDropbox',
this.userId
)
})
it('creates a notification that dropbox was unlinked', function () {
expect(
this.NotificationsBuilder.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.hooks.fire).not.to.have.been.called
})
it('does not create a notification that dropbox was unlinked', function () {
expect(this.NotificationsBuilder.dropboxDuplicateProjectNames).not.to.have
.been.called
})
}
function expectDeleteProcessed() {
it('processes the delete', function () {
expect(this.UpdateMerger.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.deleteUpdate).not.to.have.been.called
})
}
function expectProjectDeleted() {
it('deletes the project', function () {
expect(
this.ProjectDeleter.markAsDeletedByExternalSource
).to.have.been.calledWith(this.projects.active1._id)
})
}
function expectProjectNotDeleted() {
it('does not delete the project', function () {
expect(this.ProjectDeleter.markAsDeletedByExternalSource).not.to.have.been
.called
})
}