overleaf/services/web/test/unit/src/Uploads/ProjectUploadManagerTests.js

354 lines
11 KiB
JavaScript
Raw Normal View History

const sinon = require('sinon')
const { expect } = require('chai')
const timekeeper = require('timekeeper')
const SandboxedModule = require('sandboxed-module')
const { ObjectId } = require('mongodb')
const MODULE_PATH =
'../../../../app/src/Features/Uploads/ProjectUploadManager.js'
describe('ProjectUploadManager', function() {
beforeEach(function() {
this.now = Date.now()
timekeeper.freeze(this.now)
this.rootFolderId = new ObjectId()
this.ownerId = new ObjectId()
this.zipPath = '/path/to/zip/file-name.zip'
this.extractedZipPath = `/path/to/zip/file-name-${this.now}`
this.mainContent = 'Contents of main.tex'
this.projectName = 'My project*'
this.fixedProjectName = 'My project'
this.uniqueProjectName = 'My project (1)'
this.project = {
_id: new ObjectId(),
rootFolder: [{ _id: this.rootFolderId }],
overleaf: { history: { id: 12345 } }
}
this.doc = {
_id: new ObjectId(),
name: 'main.tex'
}
this.docFsPath = '/path/to/doc'
this.docLines = ['My thesis', 'by A. U. Thor']
this.file = {
_id: new ObjectId(),
name: 'image.png'
}
this.fileFsPath = '/path/to/file'
this.topLevelDestination = '/path/to/zip/file-extracted/nested'
this.newProjectVersion = 123
this.importEntries = [
{
type: 'doc',
projectPath: '/main.tex',
lines: this.docLines
},
{
type: 'file',
projectPath: `/${this.file.name}`,
fsPath: this.fileFsPath
}
]
this.docEntries = [
{
doc: this.doc,
path: `/${this.doc.name}`,
docLines: this.docLines.join('\n')
}
]
this.fileEntries = [
{ file: this.file, path: `/${this.file.name}`, url: this.fileStoreUrl }
]
this.fs = {
remove: sinon.stub().resolves()
}
this.ArchiveManager = {
promises: {
extractZipArchive: sinon.stub().resolves(),
findTopLevelDirectory: sinon
.stub()
.withArgs(this.extractedZipPath)
.resolves(this.topLevelDestination)
}
}
this.Doc = sinon.stub().returns(this.doc)
this.DocstoreManager = {
promises: {
updateDoc: sinon.stub().resolves()
}
}
this.DocumentHelper = {
getTitleFromTexContent: sinon
.stub()
.withArgs(this.mainContent)
.returns(this.projectName)
}
this.DocumentUpdaterHandler = {
promises: {
updateProjectStructure: sinon.stub().resolves()
}
}
this.FileStoreHandler = {
promises: {
uploadFileFromDisk: sinon
.stub()
.resolves({ fileRef: this.file, url: this.fileStoreUrl })
}
}
this.FileSystemImportManager = {
promises: {
importDir: sinon
.stub()
.withArgs(this.topLevelDestination)
.resolves(this.importEntries)
}
}
this.ProjectCreationHandler = {
promises: {
createBlankProject: sinon.stub().resolves(this.project)
}
}
this.ProjectEntityMongoUpdateHandler = {
promises: {
createNewFolderStructure: sinon.stub().resolves(this.newProjectVersion)
}
}
this.ProjectRootDocManager = {
promises: {
setRootDocAutomatically: sinon.stub().resolves(),
findRootDocFileFromDirectory: sinon
.stub()
.resolves({ path: 'main.tex', content: this.mainContent }),
setRootDocFromName: sinon.stub().resolves()
}
}
this.ProjectDetailsHandler = {
fixProjectName: sinon
.stub()
.withArgs(this.projectName)
.returns(this.fixedProjectName),
promises: {
generateUniqueName: sinon.stub().resolves(this.uniqueProjectName)
}
}
this.ProjectDeleter = {
promises: {
deleteProject: sinon.stub().resolves()
}
}
this.TpdsProjectFlusher = {
promises: {
flushProjectToTpds: sinon.stub().resolves()
}
}
this.ProjectUploadManager = SandboxedModule.require(MODULE_PATH, {
requires: {
'fs-extra': this.fs,
'./ArchiveManager': this.ArchiveManager,
'../../models/Doc': { Doc: this.Doc },
'../Docstore/DocstoreManager': this.DocstoreManager,
'../Documents/DocumentHelper': this.DocumentHelper,
'../DocumentUpdater/DocumentUpdaterHandler': this
.DocumentUpdaterHandler,
'../FileStore/FileStoreHandler': this.FileStoreHandler,
'./FileSystemImportManager': this.FileSystemImportManager,
'../Project/ProjectCreationHandler': this.ProjectCreationHandler,
'../Project/ProjectEntityMongoUpdateHandler': this
.ProjectEntityMongoUpdateHandler,
'../Project/ProjectRootDocManager': this.ProjectRootDocManager,
'../Project/ProjectDetailsHandler': this.ProjectDetailsHandler,
'../Project/ProjectDeleter': this.ProjectDeleter,
'../ThirdPartyDataStore/TpdsProjectFlusher': this.TpdsProjectFlusher
}
})
})
afterEach(function() {
timekeeper.reset()
})
describe('createProjectFromZipArchive', function() {
describe('when the title can be read from the root document', function() {
beforeEach(async function() {
await this.ProjectUploadManager.promises.createProjectFromZipArchive(
this.ownerId,
this.projectName,
this.zipPath
)
})
it('should extract the archive', function() {
this.ArchiveManager.promises.extractZipArchive.should.have.been.calledWith(
this.zipPath,
this.extractedZipPath
)
})
it('should create a project', function() {
this.ProjectCreationHandler.promises.createBlankProject.should.have.been.calledWith(
this.ownerId,
this.uniqueProjectName
)
})
it('should initialize the file tree', function() {
this.ProjectEntityMongoUpdateHandler.promises.createNewFolderStructure.should.have.been.calledWith(
this.project._id,
this.docEntries,
this.fileEntries
)
})
it('should notify document updater', function() {
this.DocumentUpdaterHandler.promises.updateProjectStructure.should.have.been.calledWith(
this.project._id,
this.project.overleaf.history.id,
this.ownerId,
{
newDocs: this.docEntries,
newFiles: this.fileEntries,
newProject: { version: this.newProjectVersion }
}
)
})
it('should flush the project to TPDS', function() {
this.TpdsProjectFlusher.promises.flushProjectToTpds.should.have.been.calledWith(
this.project._id
)
})
it('should set the root document', function() {
this.ProjectRootDocManager.promises.setRootDocFromName.should.have.been.calledWith(
this.project._id,
'main.tex'
)
})
it('should remove the destination directory afterwards', function() {
this.fs.remove.should.have.been.calledWith(this.extractedZipPath)
})
})
describe("when the root document can't be determined", function() {
beforeEach(async function() {
this.ProjectRootDocManager.promises.findRootDocFileFromDirectory.resolves(
{}
)
await this.ProjectUploadManager.promises.createProjectFromZipArchive(
this.ownerId,
this.projectName,
this.zipPath
)
})
it('should not try to set the root doc', function() {
this.ProjectRootDocManager.promises.setRootDocFromName.should.not.have
.been.called
})
})
})
describe('createProjectFromZipArchiveWithName', function() {
beforeEach(async function() {
await this.ProjectUploadManager.promises.createProjectFromZipArchiveWithName(
this.ownerId,
this.projectName,
this.zipPath
)
})
it('should extract the archive', function() {
this.ArchiveManager.promises.extractZipArchive.should.have.been.calledWith(
this.zipPath,
this.extractedZipPath
)
})
it('should create a project owned by the owner_id', function() {
this.ProjectCreationHandler.promises.createBlankProject.should.have.been.calledWith(
this.ownerId,
this.uniqueProjectName
)
})
it('should automatically set the root doc', function() {
this.ProjectRootDocManager.promises.setRootDocAutomatically.should.have.been.calledWith(
this.project._id
)
})
it('should initialize the file tree', function() {
this.ProjectEntityMongoUpdateHandler.promises.createNewFolderStructure.should.have.been.calledWith(
this.project._id,
this.docEntries,
this.fileEntries
)
})
it('should notify document updater', function() {
this.DocumentUpdaterHandler.promises.updateProjectStructure.should.have.been.calledWith(
this.project._id,
this.project.overleaf.history.id,
this.ownerId,
{
newDocs: this.docEntries,
newFiles: this.fileEntries,
newProject: { version: this.newProjectVersion }
}
)
})
it('should flush the project to TPDS', function() {
this.TpdsProjectFlusher.promises.flushProjectToTpds.should.have.been.calledWith(
this.project._id
)
})
it('should remove the destination directory afterwards', function() {
this.fs.remove.should.have.been.calledWith(this.extractedZipPath)
})
describe('when initializing the folder structure fails', function() {
beforeEach(async function() {
this.ProjectEntityMongoUpdateHandler.promises.createNewFolderStructure.rejects()
await expect(
this.ProjectUploadManager.promises.createProjectFromZipArchiveWithName(
this.ownerId,
this.projectName,
this.zipPath
)
).to.be.rejected
})
it('should cleanup the blank project created', async function() {
this.ProjectDeleter.promises.deleteProject.should.have.been.calledWith(
this.project._id
)
})
})
describe('when setting automatically the root doc fails', function() {
beforeEach(async function() {
this.ProjectRootDocManager.promises.setRootDocAutomatically.rejects()
await expect(
this.ProjectUploadManager.promises.createProjectFromZipArchiveWithName(
this.ownerId,
this.projectName,
this.zipPath
)
).to.be.rejected
})
it('should cleanup the blank project created', function() {
this.ProjectDeleter.promises.deleteProject.should.have.been.calledWith(
this.project._id
)
})
})
})
})