mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-05-30 20:31:34 +02:00
649 lines
17 KiB
JavaScript
649 lines
17 KiB
JavaScript
import { expect, vi } from 'vitest'
|
|
import sinon from 'sinon'
|
|
import mongodb from 'mongodb-legacy'
|
|
import Errors from '../../../../app/src/Features/Errors/Errors.js'
|
|
|
|
const ObjectId = mongodb.ObjectId
|
|
|
|
const MODULE_PATH =
|
|
'../../../../app/src/Features/ThirdPartyDataStore/TpdsUpdateHandler.mjs'
|
|
|
|
vi.mock('../../../../app/src/Features/Errors/Errors.js', () =>
|
|
vi.importActual('../../../../app/src/Features/Errors/Errors.js')
|
|
)
|
|
|
|
describe('TpdsUpdateHandler', function () {
|
|
beforeEach(async function (ctx) {
|
|
ctx.projectName = 'My recipes'
|
|
ctx.projects = {
|
|
active1: { _id: new ObjectId(), name: ctx.projectName },
|
|
active2: { _id: new ObjectId(), name: ctx.projectName },
|
|
archived1: {
|
|
_id: new ObjectId(),
|
|
name: ctx.projectName,
|
|
archived: [ctx.userId],
|
|
},
|
|
archived2: {
|
|
_id: new ObjectId(),
|
|
name: ctx.projectName,
|
|
archived: [ctx.userId],
|
|
},
|
|
}
|
|
ctx.userId = new ObjectId()
|
|
ctx.source = 'dropbox'
|
|
ctx.path = `/some/file`
|
|
ctx.update = {}
|
|
ctx.folderPath = '/some/folder'
|
|
ctx.folder = {
|
|
_id: new ObjectId(),
|
|
parentFolder_id: new ObjectId(),
|
|
}
|
|
|
|
ctx.CooldownManager = {
|
|
promises: {
|
|
isProjectOnCooldown: sinon.stub().resolves(false),
|
|
},
|
|
}
|
|
ctx.FileTypeManager = {
|
|
promises: {
|
|
shouldIgnore: sinon.stub().resolves(false),
|
|
},
|
|
}
|
|
ctx.Modules = {
|
|
promises: {
|
|
hooks: { fire: sinon.stub().resolves() },
|
|
},
|
|
}
|
|
ctx.notification = {
|
|
create: sinon.stub().resolves(),
|
|
}
|
|
ctx.NotificationsBuilder = {
|
|
promises: {
|
|
dropboxDuplicateProjectNames: sinon.stub().returns(ctx.notification),
|
|
},
|
|
}
|
|
ctx.ProjectCreationHandler = {
|
|
promises: {
|
|
createBlankProject: sinon.stub().resolves(ctx.projects.active1),
|
|
},
|
|
}
|
|
ctx.ProjectDeleter = {
|
|
promises: {
|
|
markAsDeletedByExternalSource: sinon.stub().resolves(),
|
|
},
|
|
}
|
|
ctx.ProjectGetter = {
|
|
promises: {
|
|
findUsersProjectsByName: sinon.stub(),
|
|
findAllUsersProjects: sinon
|
|
.stub()
|
|
.resolves({ owned: [ctx.projects.active1], readAndWrite: [] }),
|
|
},
|
|
}
|
|
ctx.ProjectHelper = {
|
|
isArchivedOrTrashed: sinon.stub().returns(false),
|
|
}
|
|
ctx.ProjectHelper.isArchivedOrTrashed
|
|
.withArgs(ctx.projects.archived1, ctx.userId)
|
|
.returns(true)
|
|
ctx.ProjectHelper.isArchivedOrTrashed
|
|
.withArgs(ctx.projects.archived2, ctx.userId)
|
|
.returns(true)
|
|
ctx.RootDocManager = {
|
|
setRootDocAutomaticallyInBackground: sinon.stub(),
|
|
}
|
|
ctx.UpdateMerger = {
|
|
promises: {
|
|
deleteUpdate: sinon.stub().resolves(),
|
|
mergeUpdate: sinon.stub().resolves(),
|
|
createFolder: sinon.stub().resolves(ctx.folder),
|
|
},
|
|
}
|
|
|
|
vi.doMock('../../../../app/src/Features/Cooldown/CooldownManager', () => ({
|
|
default: ctx.CooldownManager,
|
|
}))
|
|
|
|
vi.doMock('../../../../app/src/Features/Uploads/FileTypeManager', () => ({
|
|
default: ctx.FileTypeManager,
|
|
}))
|
|
|
|
vi.doMock('../../../../app/src/infrastructure/Modules', () => ({
|
|
default: ctx.Modules,
|
|
}))
|
|
|
|
vi.doMock(
|
|
'../../../../app/src/Features/Notifications/NotificationsBuilder',
|
|
() => ({
|
|
default: ctx.NotificationsBuilder,
|
|
})
|
|
)
|
|
|
|
vi.doMock(
|
|
'../../../../app/src/Features/Project/ProjectCreationHandler',
|
|
() => ({
|
|
default: ctx.ProjectCreationHandler,
|
|
})
|
|
)
|
|
|
|
vi.doMock('../../../../app/src/Features/Project/ProjectDeleter', () => ({
|
|
default: ctx.ProjectDeleter,
|
|
}))
|
|
|
|
vi.doMock('../../../../app/src/Features/Project/ProjectGetter', () => ({
|
|
default: ctx.ProjectGetter,
|
|
}))
|
|
|
|
vi.doMock('../../../../app/src/Features/Project/ProjectHelper', () => ({
|
|
default: ctx.ProjectHelper,
|
|
}))
|
|
|
|
vi.doMock(
|
|
'../../../../app/src/Features/Project/ProjectRootDocManager',
|
|
() => ({
|
|
default: ctx.RootDocManager,
|
|
})
|
|
)
|
|
|
|
vi.doMock(
|
|
'../../../../app/src/Features/ThirdPartyDataStore/UpdateMerger',
|
|
() => ({
|
|
default: ctx.UpdateMerger,
|
|
})
|
|
)
|
|
|
|
ctx.TpdsUpdateHandler = (await import(MODULE_PATH)).default
|
|
})
|
|
|
|
describe('getting an update', function () {
|
|
describe('byId', function () {
|
|
describe('with no matching project', function () {
|
|
beforeEach(function (ctx) {
|
|
ctx.projectId = new ObjectId().toString()
|
|
})
|
|
receiveUpdateById()
|
|
expectProjectNotCreated()
|
|
expectUpdateNotProcessed()
|
|
})
|
|
|
|
describe('with one matching active project', function () {
|
|
beforeEach(function (ctx) {
|
|
ctx.projectId = ctx.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 (ctx) {
|
|
ctx.FileTypeManager.promises.shouldIgnore.resolves(true)
|
|
})
|
|
receiveUpdate()
|
|
expectProjectNotCreated()
|
|
expectUpdateNotProcessed()
|
|
expectDropboxNotUnlinked()
|
|
})
|
|
|
|
describe('update to a project on cooldown', async function () {
|
|
setupMatchingProjects(['active1'])
|
|
setupProjectOnCooldown()
|
|
beforeEach(async function (ctx) {
|
|
await expect(
|
|
ctx.TpdsUpdateHandler.promises.newUpdate(
|
|
ctx.userId,
|
|
'', // projectId
|
|
ctx.projectName,
|
|
ctx.path,
|
|
ctx.update,
|
|
ctx.source
|
|
)
|
|
).to.be.rejectedWith(Errors.TooManyRequestsError)
|
|
})
|
|
expectUpdateNotProcessed()
|
|
})
|
|
})
|
|
|
|
describe('getting a file delete', function () {
|
|
describe('byId', function () {
|
|
describe('with no matching project', function () {
|
|
beforeEach(function (ctx) {
|
|
ctx.projectId = new ObjectId().toString()
|
|
})
|
|
receiveFileDeleteById()
|
|
expectDeleteNotProcessed()
|
|
expectProjectNotDeleted()
|
|
})
|
|
|
|
describe('with one matching active project', function () {
|
|
beforeEach(function (ctx) {
|
|
ctx.projectId = ctx.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 (ctx) {
|
|
await expect(
|
|
ctx.TpdsUpdateHandler.promises.createFolder(
|
|
ctx.userId,
|
|
ctx.projectId,
|
|
ctx.projectName,
|
|
ctx.path
|
|
)
|
|
).to.be.rejectedWith(Errors.TooManyRequestsError)
|
|
})
|
|
expectFolderUpdateNotProcessed()
|
|
})
|
|
})
|
|
})
|
|
|
|
/* Setup helpers */
|
|
|
|
function setupMatchingProjects(projectKeys) {
|
|
beforeEach(function (ctx) {
|
|
const projects = projectKeys.map(key => ctx.projects[key])
|
|
ctx.ProjectGetter.promises.findUsersProjectsByName
|
|
.withArgs(ctx.userId, ctx.projectName)
|
|
.resolves(projects)
|
|
})
|
|
}
|
|
|
|
function setupProjectOnCooldown() {
|
|
beforeEach(function (ctx) {
|
|
ctx.CooldownManager.promises.isProjectOnCooldown
|
|
.withArgs(ctx.projects.active1._id)
|
|
.resolves(true)
|
|
})
|
|
}
|
|
|
|
/* Test helpers */
|
|
|
|
function receiveUpdate() {
|
|
beforeEach(async function (ctx) {
|
|
await ctx.TpdsUpdateHandler.promises.newUpdate(
|
|
ctx.userId,
|
|
'', // projectId
|
|
ctx.projectName,
|
|
ctx.path,
|
|
ctx.update,
|
|
ctx.source
|
|
)
|
|
})
|
|
}
|
|
|
|
function receiveUpdateById() {
|
|
beforeEach(async function (ctx) {
|
|
await ctx.TpdsUpdateHandler.promises.newUpdate(
|
|
ctx.userId,
|
|
ctx.projectId,
|
|
'', // projectName
|
|
ctx.path,
|
|
ctx.update,
|
|
ctx.source
|
|
)
|
|
})
|
|
}
|
|
|
|
function receiveFileDelete() {
|
|
beforeEach(async function (ctx) {
|
|
await ctx.TpdsUpdateHandler.promises.deleteUpdate(
|
|
ctx.userId,
|
|
'', // projectId
|
|
ctx.projectName,
|
|
ctx.path,
|
|
ctx.source
|
|
)
|
|
})
|
|
}
|
|
|
|
function receiveFileDeleteById() {
|
|
beforeEach(async function (ctx) {
|
|
await ctx.TpdsUpdateHandler.promises.deleteUpdate(
|
|
ctx.userId,
|
|
ctx.projectId,
|
|
'', // projectName
|
|
ctx.path,
|
|
ctx.source
|
|
)
|
|
})
|
|
}
|
|
|
|
function receiveProjectDelete() {
|
|
beforeEach(async function (ctx) {
|
|
await ctx.TpdsUpdateHandler.promises.deleteUpdate(
|
|
ctx.userId,
|
|
'', // projectId
|
|
ctx.projectName,
|
|
'/',
|
|
ctx.source
|
|
)
|
|
})
|
|
}
|
|
|
|
function receiveFolderUpdate() {
|
|
beforeEach(async function (ctx) {
|
|
await ctx.TpdsUpdateHandler.promises.createFolder(
|
|
ctx.userId,
|
|
ctx.projectId,
|
|
ctx.projectName,
|
|
ctx.folderPath
|
|
)
|
|
})
|
|
}
|
|
|
|
/* Expectations */
|
|
|
|
function expectProjectCreated() {
|
|
it('creates a project', function (ctx) {
|
|
expect(
|
|
ctx.ProjectCreationHandler.promises.createBlankProject
|
|
).to.have.been.calledWith(ctx.userId, ctx.projectName)
|
|
})
|
|
|
|
it('sets the root doc', function (ctx) {
|
|
expect(
|
|
ctx.RootDocManager.setRootDocAutomaticallyInBackground
|
|
).to.have.been.calledWith(ctx.projects.active1._id)
|
|
})
|
|
}
|
|
|
|
function expectProjectNotCreated() {
|
|
it('does not create a project', function (ctx) {
|
|
expect(ctx.ProjectCreationHandler.promises.createBlankProject).not.to.have
|
|
.been.called
|
|
})
|
|
|
|
it('does not set the root doc', function (ctx) {
|
|
expect(ctx.RootDocManager.setRootDocAutomaticallyInBackground).not.to.have
|
|
.been.called
|
|
})
|
|
}
|
|
|
|
function expectUpdateProcessed() {
|
|
it('processes the update', function (ctx) {
|
|
expect(ctx.UpdateMerger.promises.mergeUpdate).to.have.been.calledWith(
|
|
ctx.userId,
|
|
ctx.projects.active1._id,
|
|
ctx.path,
|
|
ctx.update,
|
|
ctx.source
|
|
)
|
|
})
|
|
}
|
|
|
|
function expectUpdateNotProcessed() {
|
|
it('does not process the update', function (ctx) {
|
|
expect(ctx.UpdateMerger.promises.mergeUpdate).not.to.have.been.called
|
|
})
|
|
}
|
|
|
|
function expectFolderUpdateProcessed() {
|
|
it('processes the folder update', function (ctx) {
|
|
expect(ctx.UpdateMerger.promises.createFolder).to.have.been.calledWith(
|
|
ctx.projects.active1._id,
|
|
ctx.folderPath,
|
|
ctx.userId
|
|
)
|
|
})
|
|
}
|
|
|
|
function expectFolderUpdateNotProcessed() {
|
|
it("doesn't process the folder update", function (ctx) {
|
|
expect(ctx.UpdateMerger.promises.createFolder).not.to.have.been.called
|
|
})
|
|
}
|
|
|
|
function expectDropboxUnlinked() {
|
|
it('unlinks Dropbox', function (ctx) {
|
|
expect(ctx.Modules.promises.hooks.fire).to.have.been.calledWith(
|
|
'removeDropbox',
|
|
ctx.userId,
|
|
'duplicate-projects'
|
|
)
|
|
})
|
|
|
|
it('creates a notification that dropbox was unlinked', function (ctx) {
|
|
expect(
|
|
ctx.NotificationsBuilder.promises.dropboxDuplicateProjectNames
|
|
).to.have.been.calledWith(ctx.userId)
|
|
expect(ctx.notification.create).to.have.been.calledWith(ctx.projectName)
|
|
})
|
|
}
|
|
|
|
function expectDropboxNotUnlinked() {
|
|
it('does not unlink Dropbox', function (ctx) {
|
|
expect(ctx.Modules.promises.hooks.fire).not.to.have.been.called
|
|
})
|
|
|
|
it('does not create a notification that dropbox was unlinked', function (ctx) {
|
|
expect(ctx.NotificationsBuilder.promises.dropboxDuplicateProjectNames).not
|
|
.to.have.been.called
|
|
})
|
|
}
|
|
|
|
function expectDeleteProcessed() {
|
|
it('processes the delete', function (ctx) {
|
|
expect(ctx.UpdateMerger.promises.deleteUpdate).to.have.been.calledWith(
|
|
ctx.userId,
|
|
ctx.projects.active1._id,
|
|
ctx.path,
|
|
ctx.source
|
|
)
|
|
})
|
|
}
|
|
|
|
function expectDeleteNotProcessed() {
|
|
it('does not process the delete', function (ctx) {
|
|
expect(ctx.UpdateMerger.promises.deleteUpdate).not.to.have.been.called
|
|
})
|
|
}
|
|
|
|
function expectProjectDeleted() {
|
|
it('deletes the project', function (ctx) {
|
|
expect(
|
|
ctx.ProjectDeleter.promises.markAsDeletedByExternalSource
|
|
).to.have.been.calledWith(ctx.projects.active1._id)
|
|
})
|
|
}
|
|
|
|
function expectProjectNotDeleted() {
|
|
it('does not delete the project', function (ctx) {
|
|
expect(ctx.ProjectDeleter.promises.markAsDeletedByExternalSource).not.to
|
|
.have.been.called
|
|
})
|
|
}
|