Files
Verso/services/web/test/unit/src/Uploads/FileSystemImportManagerTests.js
T
Eric Mc Sween 27941dc8af Merge pull request #2760 from overleaf/em-faster-uploads
Make a single Mongo update when uploading projects

GitOrigin-RevId: de102d3e112c9014ca5885f963e35971e4db6cee
2020-04-24 03:30:55 +00:00

358 lines
9.8 KiB
JavaScript

const sinon = require('sinon')
const { expect } = require('chai')
const mockFs = require('mock-fs')
const SandboxedModule = require('sandboxed-module')
const { ObjectId } = require('mongodb')
const MODULE_PATH =
'../../../../app/src/Features/Uploads/FileSystemImportManager.js'
describe('FileSystemImportManager', function() {
beforeEach(function() {
this.projectId = new ObjectId()
this.folderId = new ObjectId()
this.newFolderId = new ObjectId()
this.userId = new ObjectId()
this.EditorController = {
promises: {
addDoc: sinon.stub().resolves(),
addFile: sinon.stub().resolves(),
upsertDoc: sinon.stub().resolves(),
upsertFile: sinon.stub().resolves(),
addFolder: sinon.stub().resolves({ _id: this.newFolderId })
}
}
this.logger = {
log() {},
err() {}
}
this.FileSystemImportManager = SandboxedModule.require(MODULE_PATH, {
globals: {
console: console
},
requires: {
'../Editor/EditorController': this.EditorController,
'logger-sharelatex': this.logger
}
})
})
describe('importDir', function() {
beforeEach(async function() {
mockFs({
'import-test': {
'main.tex': 'My thesis',
'link-to-main.tex': mockFs.symlink({ path: 'import-test/main.tex' }),
'.DS_Store': 'Should be ignored',
images: {
'cat.jpg': Buffer.from([1, 2, 3, 4])
},
'line-endings': {
'unix.txt': 'one\ntwo\nthree',
'mac.txt': 'uno\rdos\rtres',
'windows.txt': 'ein\r\nzwei\r\ndrei',
'mixed.txt': 'uno\rdue\r\ntre\nquattro'
},
encodings: {
'utf16le.txt': Buffer.from('\ufeffétonnant!', 'utf16le'),
'latin1.txt': Buffer.from('tétanisant!', 'latin1')
}
},
symlink: mockFs.symlink({ path: 'import-test' })
})
this.entries = await this.FileSystemImportManager.promises.importDir(
'import-test'
)
this.projectPaths = this.entries.map(x => x.projectPath)
})
afterEach(function() {
mockFs.restore()
})
it('should import regular docs', function() {
expect(this.entries).to.deep.include({
type: 'doc',
projectPath: '/main.tex',
lines: ['My thesis']
})
})
it('should skip symlinks inside the import folder', function() {
expect(this.projectPaths).not.to.include('/link-to-main.tex')
})
it('should skip ignored files', function() {
expect(this.projectPaths).not.to.include('/.DS_Store')
})
it('should import binary files', function() {
expect(this.entries).to.deep.include({
type: 'file',
projectPath: '/images/cat.jpg',
fsPath: 'import-test/images/cat.jpg'
})
})
it('should deal with Mac/Windows/Unix line endings', function() {
expect(this.entries).to.deep.include({
type: 'doc',
projectPath: '/line-endings/unix.txt',
lines: ['one', 'two', 'three']
})
expect(this.entries).to.deep.include({
type: 'doc',
projectPath: '/line-endings/mac.txt',
lines: ['uno', 'dos', 'tres']
})
expect(this.entries).to.deep.include({
type: 'doc',
projectPath: '/line-endings/windows.txt',
lines: ['ein', 'zwei', 'drei']
})
expect(this.entries).to.deep.include({
type: 'doc',
projectPath: '/line-endings/mixed.txt',
lines: ['uno', 'due', 'tre', 'quattro']
})
})
it('should import documents with latin1 encoding', function() {
expect(this.entries).to.deep.include({
type: 'doc',
projectPath: '/encodings/latin1.txt',
lines: ['tétanisant!']
})
})
it('should import documents with utf16-le encoding', function() {
expect(this.entries).to.deep.include({
type: 'doc',
projectPath: '/encodings/utf16le.txt',
lines: ['\ufeffétonnant!']
})
})
it('should error when the root folder is a symlink', async function() {
await expect(this.FileSystemImportManager.promises.importDir('symlink'))
.to.be.rejected
})
})
describe('addEntity', function() {
describe('with directory', function() {
beforeEach(async function() {
mockFs({
path: {
to: {
folder: {
'doc.tex': 'one\ntwo\nthree',
'image.jpg': Buffer.from([1, 2, 3, 4])
}
}
}
})
await this.FileSystemImportManager.promises.addEntity(
this.userId,
this.projectId,
this.folderId,
'folder',
'path/to/folder',
false
)
})
afterEach(function() {
mockFs.restore()
})
it('should add a folder to the project', function() {
this.EditorController.promises.addFolder.should.have.been.calledWith(
this.projectId,
this.folderId,
'folder',
'upload'
)
})
it("should add the folder's contents", function() {
this.EditorController.promises.addDoc.should.have.been.calledWith(
this.projectId,
this.newFolderId,
'doc.tex',
['one', 'two', 'three'],
'upload',
this.userId
)
this.EditorController.promises.addFile.should.have.been.calledWith(
this.projectId,
this.newFolderId,
'image.jpg',
'path/to/folder/image.jpg',
null,
'upload',
this.userId
)
})
})
describe('with binary file', function() {
beforeEach(function() {
mockFs({ 'uploaded-file': Buffer.from([1, 2, 3, 4]) })
})
afterEach(function() {
mockFs.restore()
})
describe('with replace set to false', function() {
beforeEach(async function() {
await this.FileSystemImportManager.promises.addEntity(
this.userId,
this.projectId,
this.folderId,
'image.jpg',
'uploaded-file',
false
)
})
it('should add the file', function() {
this.EditorController.promises.addFile.should.have.been.calledWith(
this.projectId,
this.folderId,
'image.jpg',
'uploaded-file',
null,
'upload',
this.userId
)
})
})
describe('with replace set to true', function() {
beforeEach(async function() {
await this.FileSystemImportManager.promises.addEntity(
this.userId,
this.projectId,
this.folderId,
'image.jpg',
'uploaded-file',
true
)
})
it('should add the file', function() {
this.EditorController.promises.upsertFile.should.have.been.calledWith(
this.projectId,
this.folderId,
'image.jpg',
'uploaded-file',
null,
'upload',
this.userId
)
})
})
})
for (const [lineEndingDescription, lineEnding] of [
['Unix', '\n'],
['Mac', '\r'],
['Windows', '\r\n']
]) {
describe(`with text file (${lineEndingDescription} line endings)`, function() {
beforeEach(function() {
mockFs({
path: {
to: { 'uploaded-file': `one${lineEnding}two${lineEnding}three` }
}
})
})
afterEach(function() {
mockFs.restore()
})
describe('with replace set to false', function() {
beforeEach(async function() {
await this.FileSystemImportManager.promises.addEntity(
this.userId,
this.projectId,
this.folderId,
'doc.tex',
'path/to/uploaded-file',
false
)
})
it('should insert the doc', function() {
this.EditorController.promises.addDoc.should.have.been.calledWith(
this.projectId,
this.folderId,
'doc.tex',
['one', 'two', 'three'],
'upload',
this.userId
)
})
})
describe('with replace set to true', function() {
beforeEach(async function() {
await this.FileSystemImportManager.promises.addEntity(
this.userId,
this.projectId,
this.folderId,
'doc.tex',
'path/to/uploaded-file',
true
)
})
it('should upsert the doc', function() {
this.EditorController.promises.upsertDoc.should.have.been.calledWith(
this.projectId,
this.folderId,
'doc.tex',
['one', 'two', 'three'],
'upload',
this.userId
)
})
})
})
}
describe('with symlink', function() {
beforeEach(function() {
mockFs({
path: { to: { symlink: mockFs.symlink({ path: '/etc/passwd' }) } }
})
})
afterEach(function() {
mockFs.restore()
})
it('should stop with an error', async function() {
await expect(
this.FileSystemImportManager.promises.addEntity(
this.userId,
this.projectId,
this.folderId,
'main.tex',
'path/to/symlink',
false
)
).to.be.rejectedWith('path is symlink')
this.EditorController.promises.addFolder.should.not.have.been.called
this.EditorController.promises.addDoc.should.not.have.been.called
this.EditorController.promises.addFile.should.not.have.been.called
})
})
})
})