Files
overleaf-cep/services/web/test/unit/src/Project/ProjectEntityMongoUpdateHandler.test.mjs
Jakob Ackermann cb0266035d [web] remove unnecessary filtering of rootFolder (#31585)
11 years ago, the db.projects collection was storing doc lines in the
file-tree/rootFolder. Any operations on the project that did not need
those lines were benefitting from excluding all those entries from the
file-tree. These days, the verbose exclusions are not useful anymore and
merely add load on mongo.

REF: 9805c6a9ff
GitOrigin-RevId: 89f544688934c1ed1ca98877ffbe8baefe66c126
2026-02-19 09:06:13 +00:00

1183 lines
34 KiB
JavaScript

import { vi, expect } from 'vitest'
import sinon from 'sinon'
import tk from 'timekeeper'
import Errors from '../../../../app/src/Features/Errors/Errors.js'
import mongodb from 'mongodb-legacy'
import { Project } from '../../../../app/src/models/Project.mjs'
const { ObjectId } = mongodb
vi.mock('../../../../app/src/Features/Errors/Errors.js', () =>
vi.importActual('../../../../app/src/Features/Errors/Errors.js')
)
const MODULE_PATH =
'../../../../app/src/Features/Project/ProjectEntityMongoUpdateHandler'
describe('ProjectEntityMongoUpdateHandler', function () {
beforeEach(async function (ctx) {
tk.freeze(new Date())
ctx.doc = {
_id: new ObjectId(),
name: 'test-doc.txt',
lines: ['hello', 'world'],
rev: 1234,
}
ctx.docPath = {
mongo: 'rootFolder.0.docs.0',
fileSystem: '/test-doc.txt',
}
ctx.file = {
_id: new ObjectId(),
name: 'something.jpg',
linkedFileData: { provider: 'url' },
hash: 'some-hash',
}
ctx.filePath = {
fileSystem: '/something.png',
mongo: 'rootFolder.0.fileRefs.0',
}
ctx.subfolder = { _id: new ObjectId(), name: 'test-subfolder' }
ctx.subfolderPath = {
fileSystem: '/test-folder/test-subfolder',
mongo: 'rootFolder.0.folders.0.folders.0',
}
ctx.notSubfolder = { _id: new ObjectId(), name: 'test-folder-2' }
ctx.notSubfolderPath = {
fileSystem: '/test-folder-2/test-subfolder',
mongo: 'rootFolder.0.folders.0.folders.0',
}
ctx.folder = {
_id: new ObjectId(),
name: 'test-folder',
folders: [ctx.subfolder],
}
ctx.folderPath = {
fileSystem: '/test-folder',
mongo: 'rootFolder.0.folders.0',
}
ctx.rootFolder = {
_id: new ObjectId(),
folders: [ctx.folder],
docs: [ctx.doc],
fileRefs: [ctx.file],
}
ctx.rootFolderPath = {
fileSystem: '/',
mongo: 'rootFolder.0',
}
ctx.project = {
_id: new ObjectId(),
name: 'project name',
rootFolder: [ctx.rootFolder],
}
ctx.Settings = { maxEntitiesPerProject: 100 }
ctx.CooldownManager = {}
ctx.LockManager = {
promises: {
runWithLock: sinon.spy((namespace, id, runner) => runner()),
},
}
ctx.FolderModel = sinon.stub()
ctx.ProjectMock = sinon.mock(Project)
ctx.ProjectEntityHandler = {
getAllEntitiesFromProject: sinon.stub(),
}
ctx.ProjectLocator = {
findElementByMongoPath: sinon.stub().throws(new Error('not found')),
promises: {
findElement: sinon.stub().rejects(new Error('not found')),
findElementByPath: sinon.stub().rejects(new Error('not found')),
},
}
ctx.ProjectLocator.promises.findElement
.withArgs({
project: ctx.project,
element_id: ctx.rootFolder._id,
type: 'folder',
})
.resolves({
element: ctx.rootFolder,
path: ctx.rootFolderPath,
})
ctx.ProjectLocator.promises.findElement
.withArgs({
project: ctx.project,
element_id: ctx.folder._id,
type: 'folder',
})
.resolves({
element: ctx.folder,
path: ctx.folderPath,
folder: ctx.rootFolder,
})
ctx.ProjectLocator.promises.findElement
.withArgs({
project: ctx.project,
element_id: ctx.subfolder._id,
type: 'folder',
})
.resolves({
element: ctx.subfolder,
path: ctx.subfolderPath,
folder: ctx.folder,
})
ctx.ProjectLocator.promises.findElement
.withArgs({
project: ctx.project,
element_id: ctx.file._id,
type: 'file',
})
.resolves({
element: ctx.file,
path: ctx.filePath,
folder: ctx.rootFolder,
})
ctx.ProjectLocator.promises.findElement
.withArgs({
project: ctx.project,
element_id: ctx.doc._id,
type: 'doc',
})
.resolves({
element: ctx.doc,
path: ctx.docPath,
folder: ctx.rootFolder,
})
ctx.ProjectLocator.promises.findElementByPath
.withArgs(
sinon.match({
project: ctx.project,
path: '/',
})
)
.resolves({ element: ctx.rootFolder, type: 'folder', folder: null })
ctx.ProjectLocator.promises.findElementByPath
.withArgs(
sinon.match({
project: ctx.project,
path: '/test-folder',
})
)
.resolves({
element: ctx.folder,
type: 'folder',
folder: ctx.rootFolder,
})
ctx.ProjectLocator.promises.findElementByPath
.withArgs(
sinon.match({
project: ctx.project,
path: '/test-folder/test-subfolder',
})
)
.resolves({
element: ctx.subfolder,
type: 'folder',
folder: ctx.folder,
})
ctx.ProjectGetter = {
promises: {
getProjectWithoutLock: sinon
.stub()
.withArgs(ctx.project._id)
.resolves(ctx.project),
getProject: sinon.stub().resolves(ctx.project),
},
}
ctx.FolderStructureBuilder = {
buildFolderStructure: sinon.stub(),
}
vi.doMock('mongodb-legacy', () => ({
default: { ObjectId },
}))
vi.doMock('@overleaf/settings', () => ({
default: ctx.Settings,
}))
vi.doMock('../../../../app/src/Features/Cooldown/CooldownManager', () => ({
default: ctx.CooldownManager,
}))
vi.doMock('../../../../app/src/models/Folder', () => ({
Folder: ctx.FolderModel,
}))
vi.doMock('../../../../app/src/infrastructure/LockManager', () => ({
default: ctx.LockManager,
}))
vi.doMock('../../../../app/src/models/Project', () => ({
Project,
}))
vi.doMock(
'../../../../app/src/Features/Project/ProjectEntityHandler',
() => ({
default: ctx.ProjectEntityHandler,
})
)
vi.doMock('../../../../app/src/Features/Project/ProjectLocator', () => ({
default: ctx.ProjectLocator,
}))
vi.doMock('../../../../app/src/Features/Project/ProjectGetter', () => ({
default: ctx.ProjectGetter,
}))
vi.doMock(
'../../../../app/src/Features/Project/FolderStructureBuilder',
() => ({
default: ctx.FolderStructureBuilder,
})
)
ctx.subject = (await import(MODULE_PATH)).default
})
afterEach(function (ctx) {
ctx.ProjectMock.restore()
tk.reset()
})
describe('addDoc', function () {
beforeEach(async function (ctx) {
const doc = { _id: new ObjectId(), name: 'other.txt' }
const userId = new ObjectId().toString()
ctx.ProjectMock.expects('findOneAndUpdate')
.withArgs(
{
_id: ctx.project._id,
'rootFolder.0.folders.0': { $exists: true },
},
{
$push: { 'rootFolder.0.folders.0.docs': doc },
$inc: { version: 1 },
$set: { lastUpdated: new Date(), lastUpdatedBy: userId },
}
)
.chain('exec')
.resolves(ctx.project)
ctx.result = await ctx.subject.promises.addDoc(
ctx.project._id,
ctx.folder._id,
doc,
userId
)
})
it('adds the document in Mongo', function (ctx) {
ctx.ProjectMock.verify()
})
it('returns path info and the project', function (ctx) {
expect(ctx.result).to.deep.equal({
result: {
path: {
mongo: 'rootFolder.0.folders.0',
fileSystem: '/test-folder/other.txt',
},
},
project: ctx.project,
})
})
})
describe('addFile', function () {
let userId
beforeEach(function (ctx) {
userId = new ObjectId().toString()
ctx.newFile = { _id: new ObjectId(), name: 'picture.jpg' }
ctx.ProjectMock.expects('findOneAndUpdate')
.withArgs(
{
_id: ctx.project._id,
'rootFolder.0.folders.0': { $exists: true },
},
{
$push: { 'rootFolder.0.folders.0.fileRefs': ctx.newFile },
$inc: { version: 1 },
$set: { lastUpdated: new Date(), lastUpdatedBy: userId },
}
)
.chain('exec')
.resolves(ctx.project)
})
describe('happy path', function () {
beforeEach(async function (ctx) {
ctx.result = await ctx.subject.promises.addFile(
ctx.project._id,
ctx.folder._id,
ctx.newFile,
userId
)
})
it('adds the file in Mongo', function (ctx) {
ctx.ProjectMock.verify()
})
it('returns path info and the project', function (ctx) {
expect(ctx.result).to.deep.equal({
result: {
path: {
mongo: 'rootFolder.0.folders.0',
fileSystem: '/test-folder/picture.jpg',
},
},
project: ctx.project,
})
})
})
describe('when entity limit is reached', function () {
beforeEach(function (ctx) {
ctx.savedMaxEntities = ctx.Settings.maxEntitiesPerProject
ctx.Settings.maxEntitiesPerProject = 3
})
afterEach(function (ctx) {
ctx.Settings.maxEntitiesPerProject = ctx.savedMaxEntities
})
it('should throw an error', async function (ctx) {
await expect(
ctx.subject.promises.addFile(
ctx.project._id,
ctx.folder._id,
ctx.newFile,
userId
)
).to.be.rejected
})
})
})
describe('addFolder', function () {
beforeEach(async function (ctx) {
const userId = new ObjectId().toString()
const folderName = 'New folder'
ctx.FolderModel.withArgs({ name: folderName }).returns({
_id: new ObjectId(),
name: folderName,
})
ctx.ProjectMock.expects('findOneAndUpdate')
.withArgs(
{
_id: ctx.project._id,
'rootFolder.0.folders.0': { $exists: true },
},
{
$push: {
'rootFolder.0.folders.0.folders': sinon.match({
name: folderName,
}),
},
$inc: { version: 1 },
$set: { lastUpdated: new Date(), lastUpdatedBy: userId },
}
)
.chain('exec')
.resolves(ctx.project)
await ctx.subject.promises.addFolder(
ctx.project._id,
ctx.folder._id,
folderName,
userId
)
})
it('adds the folder in Mongo', function (ctx) {
ctx.ProjectMock.verify()
})
})
describe('replaceFileWithNew', function () {
beforeEach(async function (ctx) {
const newFile = {
_id: new ObjectId(),
name: 'some-other-file.png',
linkedFileData: { some: 'data' },
hash: 'some-hash',
}
// Update the file in place
ctx.ProjectMock.expects('findOneAndUpdate')
.withArgs(
{
_id: ctx.project._id,
'rootFolder.0.fileRefs.0': { $exists: true },
},
{
$set: {
'rootFolder.0.fileRefs.0._id': newFile._id,
'rootFolder.0.fileRefs.0.created': sinon.match.date,
'rootFolder.0.fileRefs.0.linkedFileData': newFile.linkedFileData,
'rootFolder.0.fileRefs.0.hash': newFile.hash,
lastUpdated: new Date(),
lastUpdatedBy: 'userId',
},
$inc: {
version: 1,
'rootFolder.0.fileRefs.0.rev': 1,
},
}
)
.chain('exec')
.resolves(ctx.project)
ctx.ProjectLocator.findElementByMongoPath
.withArgs(ctx.project, 'rootFolder.0.fileRefs.0')
.returns(newFile)
await ctx.subject.promises.replaceFileWithNew(
ctx.project._id,
ctx.file._id,
newFile,
'userId'
)
})
it('updates the database', function (ctx) {
ctx.ProjectMock.verify()
})
})
describe('mkdirp', function () {
describe('when the path is just a slash', function () {
beforeEach(async function (ctx) {
ctx.result = await ctx.subject.promises.mkdirp(ctx.project._id, '/')
})
it('should return the root folder', function (ctx) {
expect(ctx.result.folder).to.deep.equal(ctx.rootFolder)
})
it('should not report a parent folder', function (ctx) {
expect(ctx.result.folder.parentFolder_id).not.to.exist
})
it('should not return new folders', function (ctx) {
expect(ctx.result.newFolders).to.have.length(0)
})
})
describe('when the folder already exists', function () {
beforeEach(async function (ctx) {
ctx.result = await ctx.subject.promises.mkdirp(
ctx.project._id,
'/test-folder'
)
})
it('should return the existing folder', function (ctx) {
expect(ctx.result.folder).to.deep.equal(ctx.folder)
})
it('should report the parent folder', function (ctx) {
expect(ctx.result.folder.parentFolder_id).to.equal(ctx.rootFolder._id)
})
it('should not return new folders', function (ctx) {
expect(ctx.result.newFolders).to.have.length(0)
})
})
describe('when the path is a new folder at the top level', function () {
beforeEach(async function (ctx) {
const userId = new ObjectId().toString()
ctx.newFolder = { _id: new ObjectId(), name: 'new-folder' }
ctx.FolderModel.returns(ctx.newFolder)
ctx.exactCaseMatch = false
ctx.ProjectMock.expects('findOneAndUpdate')
.withArgs(
{ _id: ctx.project._id, 'rootFolder.0': { $exists: true } },
{
$push: { 'rootFolder.0.folders': ctx.newFolder },
$inc: { version: 1 },
$set: { lastUpdated: new Date(), lastUpdatedBy: userId },
}
)
.chain('exec')
.resolves(ctx.project)
ctx.result = await ctx.subject.promises.mkdirp(
ctx.project._id,
'/new-folder/',
userId,
{ exactCaseMatch: ctx.exactCaseMatch }
)
})
it('should update the database', function (ctx) {
ctx.ProjectMock.verify()
})
it('should make just one folder', function (ctx) {
expect(ctx.result.newFolders).to.have.length(1)
})
it('should return the new folder', function (ctx) {
expect(ctx.result.folder.name).to.equal('new-folder')
})
it('should return the parent folder', function (ctx) {
expect(ctx.result.folder.parentFolder_id).to.equal(ctx.rootFolder._id)
})
it('should pass the exactCaseMatch option to ProjectLocator', function (ctx) {
expect(
ctx.ProjectLocator.promises.findElementByPath
).to.have.been.calledWithMatch({ exactCaseMatch: ctx.exactCaseMatch })
})
})
describe('adding a subfolder', function () {
beforeEach(async function (ctx) {
const userId = new ObjectId().toString()
ctx.newFolder = { _id: new ObjectId(), name: 'new-folder' }
ctx.FolderModel.returns(ctx.newFolder)
ctx.ProjectMock.expects('findOneAndUpdate')
.withArgs(
{
_id: ctx.project._id,
'rootFolder.0.folders.0': { $exists: true },
},
{
$push: {
'rootFolder.0.folders.0.folders': sinon.match({
name: 'new-folder',
}),
},
$inc: { version: 1 },
$set: { lastUpdated: new Date(), lastUpdatedBy: userId },
}
)
.chain('exec')
.resolves(ctx.project)
ctx.result = await ctx.subject.promises.mkdirp(
ctx.project._id,
'/test-folder/new-folder',
userId
)
})
it('should update the database', function (ctx) {
ctx.ProjectMock.verify()
})
it('should create one folder', function (ctx) {
expect(ctx.result.newFolders).to.have.length(1)
})
it('should return the new folder', function (ctx) {
expect(ctx.result.folder.name).to.equal('new-folder')
})
it('should return the parent folder', function (ctx) {
expect(ctx.result.folder.parentFolder_id).to.equal(ctx.folder._id)
})
})
describe('when mutliple folders are missing', async function () {
let userId
beforeEach(function (ctx) {
userId = new ObjectId().toString()
ctx.folder1 = { _id: new ObjectId(), name: 'folder1' }
ctx.folder1Path = {
fileSystem: '/test-folder/folder1',
mongo: 'rootFolder.0.folders.0.folders.0',
}
ctx.folder2 = { _id: new ObjectId(), name: 'folder2' }
ctx.folder2Path = {
fileSystem: '/test-folder/folder1/folder2',
mongo: 'rootFolder.0.folders.0.folders.0.folders.0',
}
ctx.FolderModel.onFirstCall().returns(ctx.folder1)
ctx.FolderModel.onSecondCall().returns(ctx.folder2)
ctx.ProjectLocator.promises.findElement
.withArgs({
project: ctx.project,
element_id: ctx.folder1._id,
type: 'folder',
})
.resolves({
element: ctx.folder1,
path: ctx.folder1Path,
})
ctx.ProjectLocator.promises.findElement
.withArgs({
project: ctx.project,
element_id: ctx.folder2._id,
type: 'folder',
})
.resolves({
element: ctx.folder2,
path: ctx.folder2Path,
})
ctx.ProjectMock.expects('findOneAndUpdate')
.withArgs(
{
_id: ctx.project._id,
'rootFolder.0.folders.0': { $exists: true },
},
{
$push: {
'rootFolder.0.folders.0.folders': sinon.match({
name: 'folder1',
}),
},
$inc: { version: 1 },
$set: { lastUpdated: new Date(), lastUpdatedBy: userId },
}
)
.chain('exec')
.resolves(ctx.project)
ctx.ProjectMock.expects('findOneAndUpdate')
.withArgs(
{
_id: ctx.project._id,
'rootFolder.0.folders.0.folders.0': { $exists: true },
},
{
$push: {
'rootFolder.0.folders.0.folders.0.folders': sinon.match({
name: 'folder2',
}),
},
$inc: { version: 1 },
$set: { lastUpdated: new Date(), lastUpdatedBy: userId },
}
)
.chain('exec')
.resolves(ctx.project)
})
;[
{
description: 'without a trailing slash',
path: '/test-folder/folder1/folder2',
},
{
description: 'with a trailing slash',
path: '/test-folder/folder1/folder2/',
},
].forEach(({ description, path }) => {
describe(description, function () {
beforeEach(async function (ctx) {
ctx.result = await ctx.subject.promises.mkdirp(
ctx.project._id,
path,
userId
)
})
it('should update the database', function (ctx) {
ctx.ProjectMock.verify()
})
it('should add multiple folders', function (ctx) {
const newFolders = ctx.result.newFolders
expect(newFolders).to.have.length(2)
expect(newFolders[0].name).to.equal('folder1')
expect(newFolders[1].name).to.equal('folder2')
})
it('should return the last folder', function (ctx) {
expect(ctx.result.folder.name).to.equal('folder2')
})
it('should return the parent folder', function (ctx) {
expect(ctx.result.folder.parentFolder_id).to.equal(ctx.folder1._id)
})
})
})
})
})
describe('moveEntity', function () {
describe('moving a doc into a different folder', function () {
beforeEach(async function (ctx) {
const userId = new ObjectId().toString()
ctx.pathAfterMove = {
fileSystem: '/somewhere/else.txt',
}
ctx.oldDocs = ['old-doc']
ctx.oldFiles = ['old-file']
ctx.newDocs = ['new-doc']
ctx.newFiles = ['new-file']
ctx.ProjectEntityHandler.getAllEntitiesFromProject
.onFirstCall()
.returns({ docs: ctx.oldDocs, files: ctx.oldFiles })
ctx.ProjectEntityHandler.getAllEntitiesFromProject
.onSecondCall()
.returns({ docs: ctx.newDocs, files: ctx.newFiles })
ctx.ProjectMock.expects('findOneAndUpdate')
.withArgs(
{
_id: ctx.project._id,
'rootFolder.0.folders.0': { $exists: true },
},
{
$push: { 'rootFolder.0.folders.0.docs': ctx.doc },
$inc: { version: 1 },
$set: { lastUpdated: new Date(), lastUpdatedBy: userId },
}
)
.chain('exec')
.resolves(ctx.project)
ctx.ProjectMock.expects('findOneAndUpdate')
.withArgs(
{ _id: ctx.project._id },
{
$pull: { 'rootFolder.0.docs': { _id: ctx.doc._id } },
$inc: { version: 1 },
$set: { lastUpdated: new Date(), lastUpdatedBy: userId },
}
)
.chain('exec')
.resolves(ctx.project)
ctx.result = await ctx.subject.promises.moveEntity(
ctx.project._id,
ctx.doc._id,
ctx.folder._id,
'doc',
userId
)
})
it('should update the database', function (ctx) {
ctx.ProjectMock.verify()
})
it('should report what changed', function (ctx) {
expect(ctx.result).to.deep.equal({
project: ctx.project,
startPath: '/test-doc.txt',
endPath: '/test-folder/test-doc.txt',
rev: ctx.doc.rev,
changes: {
oldDocs: ctx.oldDocs,
newDocs: ctx.newDocs,
oldFiles: ctx.oldFiles,
newFiles: ctx.newFiles,
newProject: ctx.project,
},
})
})
})
describe('when moving a folder inside itself', function () {
it('throws an error', async function (ctx) {
await expect(
ctx.subject.promises.moveEntity(
ctx.project._id,
ctx.folder._id,
ctx.folder._id,
'folder'
)
).to.be.rejectedWith(Errors.InvalidNameError)
})
})
describe('when moving a folder to a subfolder of itself', function () {
it('throws an error', async function (ctx) {
await expect(
ctx.subject.promises.moveEntity(
ctx.project._id,
ctx.folder._id,
ctx.subfolder._id,
'folder'
)
).to.be.rejectedWith(Errors.InvalidNameError)
})
})
describe('when moving a folder to a subfolder which starts with the same characters', function () {
it('does not throw an error', async function (ctx) {
await expect(
ctx.subject.promises.moveEntity(
ctx.project._id,
ctx.folder._id,
ctx.notSubfolder._id,
'folder'
)
).not.to.be.rejectedWith(Errors.InvalidNameError)
})
})
})
describe('deleteEntity', function () {
beforeEach(async function (ctx) {
const userId = new ObjectId().toString()
ctx.ProjectMock.expects('findOneAndUpdate')
.withArgs(
{ _id: ctx.project._id },
{
$pull: { 'rootFolder.0.docs': { _id: ctx.doc._id } },
$inc: { version: 1 },
$set: { lastUpdated: new Date(), lastUpdatedBy: userId },
}
)
.chain('exec')
.resolves(ctx.project)
await ctx.subject.promises.deleteEntity(
ctx.project._id,
ctx.doc._id,
'doc',
userId
)
})
it('should update the database', function (ctx) {
ctx.ProjectMock.verify()
})
})
describe('renameEntity', function () {
describe('happy path', function () {
beforeEach(async function (ctx) {
const userId = new ObjectId().toString()
ctx.newName = 'new.tex'
ctx.oldDocs = ['old-doc']
ctx.oldFiles = ['old-file']
ctx.newDocs = ['new-doc']
ctx.newFiles = ['new-file']
ctx.ProjectEntityHandler.getAllEntitiesFromProject
.onFirstCall()
.returns({ docs: ctx.oldDocs, files: ctx.oldFiles })
ctx.ProjectEntityHandler.getAllEntitiesFromProject
.onSecondCall()
.returns({ docs: ctx.newDocs, files: ctx.newFiles })
ctx.ProjectMock.expects('findOneAndUpdate')
.withArgs(
{ _id: ctx.project._id, 'rootFolder.0.docs.0': { $exists: true } },
{
$set: {
'rootFolder.0.docs.0.name': ctx.newName,
lastUpdated: new Date(),
lastUpdatedBy: userId,
},
$inc: { version: 1 },
}
)
.chain('exec')
.resolves(ctx.project)
ctx.result = await ctx.subject.promises.renameEntity(
ctx.project._id,
ctx.doc._id,
'doc',
ctx.newName,
userId
)
})
it('should update the database', function (ctx) {
ctx.ProjectMock.verify()
})
it('returns info', function (ctx) {
expect(ctx.result).to.deep.equal({
project: ctx.project,
startPath: '/test-doc.txt',
endPath: '/new.tex',
rev: ctx.doc.rev,
changes: {
oldDocs: ctx.oldDocs,
newDocs: ctx.newDocs,
oldFiles: ctx.oldFiles,
newFiles: ctx.newFiles,
newProject: ctx.project,
},
})
})
})
describe('name already exists', function () {
it('should throw an error', async function (ctx) {
await expect(
ctx.subject.promises.renameEntity(
ctx.project._id,
ctx.doc._id,
'doc',
ctx.folder.name
)
).to.be.rejectedWith(Errors.DuplicateNameError)
})
})
})
describe('_putElement', function () {
describe('updating the project', function () {
describe('when the parent folder is given', function () {
let userId
beforeEach(function (ctx) {
userId = new ObjectId().toString()
ctx.newFile = { _id: new ObjectId(), name: 'new file.png' }
ctx.ProjectMock.expects('findOneAndUpdate')
.withArgs(
{
_id: ctx.project._id,
'rootFolder.0.folders.0': { $exists: true },
},
{
$push: { 'rootFolder.0.folders.0.fileRefs': ctx.newFile },
$inc: { version: 1 },
$set: { lastUpdated: new Date(), lastUpdatedBy: userId },
}
)
.chain('exec')
.resolves(ctx.project)
})
it('should update the database', async function (ctx) {
await ctx.subject.promises._putElement(
ctx.project,
ctx.folder._id,
ctx.newFile,
'files',
userId
)
ctx.ProjectMock.verify()
})
it('should add an s onto the type if not included', async function (ctx) {
await ctx.subject.promises._putElement(
ctx.project,
ctx.folder._id,
ctx.newFile,
'file',
userId
)
ctx.ProjectMock.verify()
})
})
describe('error cases', function () {
it('should throw an error if element is null', async function (ctx) {
await expect(
ctx.subject.promises._putElement(
ctx.project,
ctx.folder._id,
null,
'file'
)
).to.be.rejected
})
it('should error if the element has no _id', async function (ctx) {
const file = { name: 'something' }
await expect(
ctx.subject.promises._putElement(
ctx.project,
ctx.folder._id,
file,
'file'
)
).to.be.rejected
})
it('should error if element name contains invalid characters', async function (ctx) {
const file = { _id: new ObjectId(), name: 'something*bad' }
await expect(
ctx.subject.promises._putElement(
ctx.project,
ctx.folder._id,
file,
'file'
)
).to.be.rejected
})
it('should error if element name is too long', async function (ctx) {
const file = {
_id: new ObjectId(),
name: 'long-'.repeat(1000) + 'something',
}
await expect(
ctx.subject.promises._putElement(
ctx.project,
ctx.folder._id,
file,
'file'
)
).to.be.rejectedWith(Errors.InvalidNameError)
})
it('should error if the folder name is too long', async function (ctx) {
const file = {
_id: new ObjectId(),
name: 'something',
}
ctx.ProjectLocator.promises.findElement
.withArgs({
project: ctx.project,
element_id: ctx.folder._id,
type: 'folder',
})
.resolves({
element: ctx.folder,
path: { fileSystem: 'subdir/'.repeat(1000) + 'foo' },
})
await expect(
ctx.subject.promises._putElement(
ctx.project,
ctx.folder._id,
file,
'file'
)
).to.be.rejectedWith(Errors.InvalidNameError)
})
;['file', 'doc', 'folder'].forEach(entityType => {
it(`should error if a ${entityType} already exists with the same name`, async function (ctx) {
const file = {
_id: new ObjectId(),
name: ctx[entityType].name,
}
await expect(
ctx.subject.promises._putElement(ctx.project, null, file, 'file')
).to.be.rejectedWith(Errors.DuplicateNameError)
})
})
})
})
describe('when the parent folder is not given', function () {
it('should default to root folder insert', async function (ctx) {
const userId = new ObjectId().toString()
ctx.newFile = { _id: new ObjectId(), name: 'new file.png' }
ctx.ProjectMock.expects('findOneAndUpdate')
.withArgs(
{ _id: ctx.project._id, 'rootFolder.0': { $exists: true } },
{
$push: { 'rootFolder.0.fileRefs': ctx.newFile },
$inc: { version: 1 },
$set: { lastUpdated: new Date(), lastUpdatedBy: userId },
}
)
.chain('exec')
.resolves(ctx.project)
await ctx.subject.promises._putElement(
ctx.project,
ctx.rootFolder._id,
ctx.newFile,
'file',
userId
)
})
})
})
describe('createNewFolderStructure', function () {
beforeEach(function (ctx) {
ctx.mockRootFolder = 'MOCK_ROOT_FOLDER'
ctx.docUploads = ['MOCK_DOC_UPLOAD']
ctx.fileUploads = ['MOCK_FILE_UPLOAD']
ctx.FolderStructureBuilder.buildFolderStructure
.withArgs(ctx.docUploads, ctx.fileUploads)
.returns(ctx.mockRootFolder)
ctx.updateExpectation = ctx.ProjectMock.expects('findOneAndUpdate')
.withArgs(
{
_id: ctx.project._id,
'rootFolder.0.folders.0': { $exists: false },
'rootFolder.0.docs.0': { $exists: false },
'rootFolder.0.files.0': { $exists: false },
},
{ $set: { rootFolder: [ctx.mockRootFolder] }, $inc: { version: 1 } },
{ new: true, lean: true, fields: { version: 1 } }
)
.chain('exec')
})
describe('happy path', function () {
beforeEach(async function (ctx) {
ctx.updateExpectation.resolves({ version: 1 })
await ctx.subject.promises.createNewFolderStructure(
ctx.project._id,
ctx.docUploads,
ctx.fileUploads
)
})
it('updates the database', function (ctx) {
ctx.ProjectMock.verify()
})
})
describe("when the update doesn't find a matching document", function () {
beforeEach(async function (ctx) {
ctx.updateExpectation.resolves(null)
})
it('throws an error', async function (ctx) {
await expect(
ctx.subject.promises.createNewFolderStructure(
ctx.project._id,
ctx.docUploads,
ctx.fileUploads
)
).to.be.rejected
})
})
})
describe('replaceDocWithFile', function () {
it('should simultaneously remove the doc and add the file', async function (ctx) {
const userId = new ObjectId().toString()
ctx.ProjectMock.expects('findOneAndUpdate')
.withArgs(
{ _id: ctx.project._id, 'rootFolder.0': { $exists: true } },
{
$pull: { 'rootFolder.0.docs': { _id: ctx.doc._id } },
$push: { 'rootFolder.0.fileRefs': ctx.file },
$inc: { version: 1 },
$set: { lastUpdated: new Date(), lastUpdatedBy: userId },
},
{ new: true }
)
.chain('exec')
.resolves(ctx.project)
await ctx.subject.promises.replaceDocWithFile(
ctx.project._id,
ctx.doc._id,
ctx.file,
userId
)
ctx.ProjectMock.verify()
})
})
describe('replaceFileWithDoc', function () {
it('should simultaneously remove the file and add the doc', async function (ctx) {
const userId = new ObjectId().toString()
ctx.ProjectMock.expects('findOneAndUpdate')
.withArgs(
{ _id: ctx.project._id, 'rootFolder.0': { $exists: true } },
{
$pull: { 'rootFolder.0.fileRefs': { _id: ctx.file._id } },
$push: { 'rootFolder.0.docs': ctx.doc },
$inc: { version: 1 },
$set: { lastUpdated: new Date(), lastUpdatedBy: userId },
},
{ new: true }
)
.chain('exec')
.resolves(ctx.project)
await ctx.subject.promises.replaceFileWithDoc(
ctx.project._id,
ctx.file._id,
ctx.doc,
userId
)
ctx.ProjectMock.verify()
})
})
})