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() }) }) })