first commit

This commit is contained in:
2025-04-24 13:11:28 +08:00
commit ff9c54d5e4
5960 changed files with 834111 additions and 0 deletions

View File

@@ -0,0 +1,336 @@
const sinon = require('sinon')
const chai = require('chai')
const { expect } = chai
const SandboxedModule = require('sandboxed-module')
const Errors = require('../../../app/js/Errors')
const modulePath = '../../../app/js/FileController.js'
describe('FileController', function () {
let FileHandler, LocalFileWriter, FileController, req, res, next, stream
const settings = {
s3: {
buckets: {
user_files: 'user_files',
},
},
}
const fileSize = 1234
const fileStream = {
destroy() {},
}
const projectId = 'projectId'
const fileId = 'file_id'
const bucket = 'user_files'
const key = `${projectId}/${fileId}`
const error = new Error('incorrect utensil')
beforeEach(function () {
FileHandler = {
copyObject: sinon.stub().yields(),
getFile: sinon.stub().yields(null, fileStream),
getFileSize: sinon.stub().yields(null, fileSize),
deleteFile: sinon.stub().yields(),
deleteProject: sinon.stub().yields(),
insertFile: sinon.stub().yields(),
getDirectorySize: sinon.stub().yields(null, fileSize),
getRedirectUrl: sinon.stub().yields(null, null),
}
LocalFileWriter = {}
stream = {
pipeline: sinon.stub(),
}
FileController = SandboxedModule.require(modulePath, {
requires: {
'./LocalFileWriter': LocalFileWriter,
'./FileHandler': FileHandler,
'./Errors': Errors,
stream,
'@overleaf/settings': settings,
'@overleaf/metrics': {
inc() {},
},
},
globals: { console },
})
req = {
key,
bucket,
project_id: projectId,
query: {},
params: {
project_id: projectId,
file_id: fileId,
},
headers: {},
requestLogger: {
setMessage: sinon.stub(),
addFields: sinon.stub(),
},
}
res = {
set: sinon.stub().returnsThis(),
sendStatus: sinon.stub().returnsThis(),
status: sinon.stub().returnsThis(),
}
next = sinon.stub()
})
describe('getFile', function () {
it('should try and get a redirect url first', function () {
FileController.getFile(req, res, next)
expect(FileHandler.getRedirectUrl).to.have.been.calledWith(bucket, key)
})
it('should pipe the stream', function () {
FileController.getFile(req, res, next)
expect(stream.pipeline).to.have.been.calledWith(fileStream, res)
})
it('should send a 200 if the cacheWarm param is true', function (done) {
req.query.cacheWarm = true
res.sendStatus = statusCode => {
statusCode.should.equal(200)
done()
}
FileController.getFile(req, res, next)
})
it('should send an error if there is a problem', function () {
FileHandler.getFile.yields(error)
FileController.getFile(req, res, next)
expect(next).to.have.been.calledWith(error)
})
describe('with a redirect url', function () {
const redirectUrl = 'https://wombat.potato/giraffe'
beforeEach(function () {
FileHandler.getRedirectUrl.yields(null, redirectUrl)
res.redirect = sinon.stub()
})
it('should redirect', function () {
FileController.getFile(req, res, next)
expect(res.redirect).to.have.been.calledWith(redirectUrl)
})
it('should not get a file stream', function () {
FileController.getFile(req, res, next)
expect(FileHandler.getFile).not.to.have.been.called
})
describe('when there is an error getting the redirect url', function () {
beforeEach(function () {
FileHandler.getRedirectUrl.yields(new Error('wombat herding error'))
})
it('should not redirect', function () {
FileController.getFile(req, res, next)
expect(res.redirect).not.to.have.been.called
})
it('should not return an error', function () {
FileController.getFile(req, res, next)
expect(next).not.to.have.been.called
})
it('should proxy the file', function () {
FileController.getFile(req, res, next)
expect(FileHandler.getFile).to.have.been.calledWith(bucket, key)
})
})
})
describe('with a range header', function () {
let expectedOptions
beforeEach(function () {
expectedOptions = {
bucket,
key,
format: undefined,
style: undefined,
}
})
it('should pass range options to FileHandler', function () {
req.headers.range = 'bytes=0-8'
expectedOptions.start = 0
expectedOptions.end = 8
FileController.getFile(req, res, next)
expect(FileHandler.getFile).to.have.been.calledWith(
bucket,
key,
expectedOptions
)
})
it('should ignore an invalid range header', function () {
req.headers.range = 'potato'
FileController.getFile(req, res, next)
expect(FileHandler.getFile).to.have.been.calledWith(
bucket,
key,
expectedOptions
)
})
it("should ignore any type other than 'bytes'", function () {
req.headers.range = 'wombats=0-8'
FileController.getFile(req, res, next)
expect(FileHandler.getFile).to.have.been.calledWith(
bucket,
key,
expectedOptions
)
})
})
})
describe('getFileHead', function () {
it('should return the file size in a Content-Length header', function (done) {
res.end = () => {
expect(res.status).to.have.been.calledWith(200)
expect(res.set).to.have.been.calledWith('Content-Length', fileSize)
done()
}
FileController.getFileHead(req, res, next)
})
it('should return a 404 is the file is not found', function (done) {
FileHandler.getFileSize.yields(
new Errors.NotFoundError({ message: 'not found', info: {} })
)
res.sendStatus = code => {
expect(code).to.equal(404)
done()
}
FileController.getFileHead(req, res, next)
})
it('should send an error on internal errors', function () {
FileHandler.getFileSize.yields(error)
FileController.getFileHead(req, res, next)
expect(next).to.have.been.calledWith(error)
})
})
describe('insertFile', function () {
it('should send bucket name key and res to FileHandler', function (done) {
res.sendStatus = code => {
expect(FileHandler.insertFile).to.have.been.calledWith(bucket, key, req)
expect(code).to.equal(200)
done()
}
FileController.insertFile(req, res, next)
})
})
describe('copyFile', function () {
const oldFileId = 'oldFileId'
const oldProjectId = 'oldProjectid'
const oldKey = `${oldProjectId}/${oldFileId}`
beforeEach(function () {
req.body = {
source: {
project_id: oldProjectId,
file_id: oldFileId,
},
}
})
it('should send bucket name and both keys to FileHandler', function (done) {
res.sendStatus = code => {
code.should.equal(200)
expect(FileHandler.copyObject).to.have.been.calledWith(
bucket,
oldKey,
key
)
done()
}
FileController.copyFile(req, res, next)
})
it('should send a 404 if the original file was not found', function (done) {
FileHandler.copyObject.yields(
new Errors.NotFoundError({ message: 'not found', info: {} })
)
res.sendStatus = code => {
code.should.equal(404)
done()
}
FileController.copyFile(req, res, next)
})
it('should send an error if there was an error', function (done) {
FileHandler.copyObject.yields(error)
FileController.copyFile(req, res, err => {
expect(err).to.equal(error)
done()
})
})
})
describe('delete file', function () {
it('should tell the file handler', function (done) {
res.sendStatus = code => {
code.should.equal(204)
expect(FileHandler.deleteFile).to.have.been.calledWith(bucket, key)
done()
}
FileController.deleteFile(req, res, next)
})
it('should send a 500 if there was an error', function () {
FileHandler.deleteFile.yields(error)
FileController.deleteFile(req, res, next)
expect(next).to.have.been.calledWith(error)
})
})
describe('delete project', function () {
it('should tell the file handler', function (done) {
res.sendStatus = code => {
code.should.equal(204)
expect(FileHandler.deleteProject).to.have.been.calledWith(bucket, key)
done()
}
FileController.deleteProject(req, res, next)
})
it('should send a 500 if there was an error', function () {
FileHandler.deleteProject.yields(error)
FileController.deleteProject(req, res, next)
expect(next).to.have.been.calledWith(error)
})
})
describe('directorySize', function () {
it('should return total directory size bytes', function (done) {
FileController.directorySize(req, {
json: result => {
expect(result['total bytes']).to.equal(fileSize)
done()
},
})
})
it('should send a 500 if there was an error', function () {
FileHandler.getDirectorySize.yields(error)
FileController.directorySize(req, res, next)
expect(next).to.have.been.calledWith(error)
})
})
})

View File

@@ -0,0 +1,107 @@
const sinon = require('sinon')
const chai = require('chai')
const { expect } = chai
const SandboxedModule = require('sandboxed-module')
const { Errors } = require('@overleaf/object-persistor')
const modulePath = '../../../app/js/FileConverter.js'
describe('FileConverter', function () {
let SafeExec, FileConverter
const sourcePath = '/data/wombat.eps'
const destPath = '/tmp/dest.png'
const format = 'png'
const errorMessage = 'guru meditation error'
const Settings = {
commands: {
convertCommandPrefix: [],
},
}
beforeEach(function () {
SafeExec = {
promises: sinon.stub().resolves(destPath),
}
const ObjectPersistor = { Errors }
FileConverter = SandboxedModule.require(modulePath, {
requires: {
'./SafeExec': SafeExec,
'@overleaf/metrics': {
inc: sinon.stub(),
Timer: sinon.stub().returns({ done: sinon.stub() }),
},
'@overleaf/settings': Settings,
'@overleaf/object-persistor': ObjectPersistor,
},
})
})
describe('convert', function () {
it('should convert the source to the requested format', async function () {
await FileConverter.promises.convert(sourcePath, format)
const args = SafeExec.promises.args[0][0]
expect(args).to.include(`${sourcePath}[0]`)
expect(args).to.include(`${sourcePath}.${format}`)
})
it('should return the dest path', async function () {
const destPath = await FileConverter.promises.convert(sourcePath, format)
destPath.should.equal(`${sourcePath}.${format}`)
})
it('should wrap the error from convert', async function () {
SafeExec.promises.rejects(errorMessage)
try {
await FileConverter.promises.convert(sourcePath, format)
expect('error should have been thrown').not.to.exist
} catch (err) {
expect(err.name).to.equal('ConversionError')
expect(err.cause.toString()).to.equal(errorMessage)
}
})
it('should not accept an non approved format', async function () {
try {
await FileConverter.promises.convert(sourcePath, 'potato')
expect('error should have been thrown').not.to.exist
} catch (err) {
expect(err.name).to.equal('ConversionError')
}
})
it('should prefix the command with Settings.commands.convertCommandPrefix', async function () {
Settings.commands.convertCommandPrefix = ['nice']
await FileConverter.promises.convert(sourcePath, format)
})
it('should convert the file when called as a callback', function (done) {
FileConverter.convert(sourcePath, format, (err, destPath) => {
expect(err).not.to.exist
destPath.should.equal(`${sourcePath}.${format}`)
const args = SafeExec.promises.args[0][0]
expect(args).to.include(`${sourcePath}[0]`)
expect(args).to.include(`${sourcePath}.${format}`)
done()
})
})
})
describe('thumbnail', function () {
it('should call converter resize with args', async function () {
await FileConverter.promises.thumbnail(sourcePath)
const args = SafeExec.promises.args[0][0]
expect(args).to.include(`${sourcePath}[0]`)
})
})
describe('preview', function () {
it('should call converter resize with args', async function () {
await FileConverter.promises.preview(sourcePath)
const args = SafeExec.promises.args[0][0]
expect(args).to.include(`${sourcePath}[0]`)
})
})
})

View File

@@ -0,0 +1,405 @@
const sinon = require('sinon')
const chai = require('chai')
const { expect } = chai
const modulePath = '../../../app/js/FileHandler.js'
const SandboxedModule = require('sandboxed-module')
const { ObjectId } = require('mongodb')
const { Errors } = require('@overleaf/object-persistor')
chai.use(require('sinon-chai'))
chai.use(require('chai-as-promised'))
describe('FileHandler', function () {
let PersistorManager,
LocalFileWriter,
FileConverter,
KeyBuilder,
ImageOptimiser,
FileHandler,
Settings,
fs
const bucket = 'my_bucket'
const key = `${new ObjectId()}/${new ObjectId()}`
const convertedFolderKey = `${new ObjectId()}/${new ObjectId()}`
const projectKey = `${new ObjectId()}/`
const sourceStream = 'sourceStream'
const convertedKey = 'convertedKey'
const redirectUrl = 'https://wombat.potato/giraffe'
const readStream = {
stream: 'readStream',
on: sinon.stub(),
}
beforeEach(function () {
PersistorManager = {
getObjectStream: sinon.stub().resolves(sourceStream),
getRedirectUrl: sinon.stub().resolves(redirectUrl),
checkIfObjectExists: sinon.stub().resolves(),
deleteObject: sinon.stub().resolves(),
deleteDirectory: sinon.stub().resolves(),
sendStream: sinon.stub().resolves(),
insertFile: sinon.stub().resolves(),
sendFile: sinon.stub().resolves(),
directorySize: sinon.stub().resolves(),
}
LocalFileWriter = {
// the callback style is used for detached cleanup calls
deleteFile: sinon.stub().yields(),
promises: {
writeStream: sinon.stub().resolves(),
deleteFile: sinon.stub().resolves(),
},
}
FileConverter = {
promises: {
convert: sinon.stub().resolves(),
thumbnail: sinon.stub().resolves(),
preview: sinon.stub().resolves(),
},
}
KeyBuilder = {
addCachingToKey: sinon.stub().returns(convertedKey),
getConvertedFolderKey: sinon.stub().returns(convertedFolderKey),
}
ImageOptimiser = {
promises: {
compressPng: sinon.stub().resolves(),
},
}
Settings = {
filestore: {
stores: { template_files: 'template_files', user_files: 'user_files' },
},
}
fs = {
createReadStream: sinon.stub().returns(readStream),
}
const ObjectPersistor = { Errors }
FileHandler = SandboxedModule.require(modulePath, {
requires: {
'./PersistorManager': PersistorManager,
'./LocalFileWriter': LocalFileWriter,
'./FileConverter': FileConverter,
'./KeyBuilder': KeyBuilder,
'./ImageOptimiser': ImageOptimiser,
'@overleaf/settings': Settings,
'@overleaf/object-persistor': ObjectPersistor,
'@overleaf/metrics': {
gauge: sinon.stub(),
Timer: sinon.stub().returns({ done: sinon.stub() }),
},
fs,
},
globals: { console, process },
})
})
describe('insertFile', function () {
const stream = 'stream'
it('should send file to the filestore', function (done) {
FileHandler.insertFile(bucket, key, stream, err => {
expect(err).not.to.exist
expect(PersistorManager.sendStream).to.have.been.calledWith(
bucket,
key,
stream
)
done()
})
})
it('should not make a delete request for the convertedKey folder', function (done) {
FileHandler.insertFile(bucket, key, stream, err => {
expect(err).not.to.exist
expect(PersistorManager.deleteDirectory).not.to.have.been.called
done()
})
})
it('should accept templates-api key format', function (done) {
KeyBuilder.getConvertedFolderKey.returns(
'5ecba29f1a294e007d0bccb4/v/0/pdf'
)
FileHandler.insertFile(bucket, key, stream, err => {
expect(err).not.to.exist
done()
})
})
it('should throw an error when the key is in the wrong format', function (done) {
KeyBuilder.getConvertedFolderKey.returns('wombat')
FileHandler.insertFile(bucket, key, stream, err => {
expect(err).to.exist
done()
})
})
})
describe('deleteFile', function () {
it('should tell the filestore manager to delete the file', function (done) {
FileHandler.deleteFile(bucket, key, err => {
expect(err).not.to.exist
expect(PersistorManager.deleteObject).to.have.been.calledWith(
bucket,
key
)
done()
})
})
it('should not tell the filestore manager to delete the cached folder', function (done) {
FileHandler.deleteFile(bucket, key, err => {
expect(err).not.to.exist
expect(PersistorManager.deleteDirectory).not.to.have.been.called
done()
})
})
it('should accept templates-api key format', function (done) {
KeyBuilder.getConvertedFolderKey.returns(
'5ecba29f1a294e007d0bccb4/v/0/pdf'
)
FileHandler.deleteFile(bucket, key, err => {
expect(err).not.to.exist
done()
})
})
it('should throw an error when the key is in the wrong format', function (done) {
KeyBuilder.getConvertedFolderKey.returns('wombat')
FileHandler.deleteFile(bucket, key, err => {
expect(err).to.exist
done()
})
})
describe('when conversions are enabled', function () {
beforeEach(function () {
Settings.enableConversions = true
})
it('should delete the convertedKey folder for template files', function (done) {
FileHandler.deleteFile(
Settings.filestore.stores.template_files,
key,
err => {
expect(err).not.to.exist
expect(PersistorManager.deleteDirectory).to.have.been.calledWith(
Settings.filestore.stores.template_files,
convertedFolderKey
)
done()
}
)
})
it('should not delete the convertedKey folder for user files', function (done) {
FileHandler.deleteFile(
Settings.filestore.stores.user_files,
key,
err => {
expect(err).not.to.exist
expect(PersistorManager.deleteDirectory).to.not.have.been.called
done()
}
)
})
})
})
describe('deleteProject', function () {
it('should tell the filestore manager to delete the folder', function (done) {
FileHandler.deleteProject(bucket, projectKey, err => {
expect(err).not.to.exist
expect(PersistorManager.deleteDirectory).to.have.been.calledWith(
bucket,
projectKey
)
done()
})
})
it('should throw an error when the key is in the wrong format', function (done) {
FileHandler.deleteProject(bucket, 'wombat', err => {
expect(err).to.exist
done()
})
})
})
describe('getFile', function () {
it('should return the source stream no format or style are defined', function (done) {
FileHandler.getFile(bucket, key, null, (err, stream) => {
expect(err).not.to.exist
expect(stream).to.equal(sourceStream)
done()
})
})
it('should pass options through to PersistorManager', function (done) {
const options = { start: 0, end: 8 }
FileHandler.getFile(bucket, key, options, err => {
expect(err).not.to.exist
expect(PersistorManager.getObjectStream).to.have.been.calledWith(
bucket,
key,
options
)
done()
})
})
describe('when a format is defined', function () {
let result
describe('when the file is not cached', function () {
beforeEach(function (done) {
FileHandler.getFile(bucket, key, { format: 'png' }, (err, stream) => {
result = { err, stream }
done()
})
})
it('should convert the file', function () {
expect(FileConverter.promises.convert).to.have.been.called
})
it('should compress the converted file', function () {
expect(ImageOptimiser.promises.compressPng).to.have.been.called
})
it('should return the the converted stream', function () {
expect(result.err).not.to.exist
expect(result.stream).to.equal(readStream)
expect(PersistorManager.getObjectStream).to.have.been.calledWith(
bucket,
key
)
})
})
describe('when the file is cached', function () {
beforeEach(function (done) {
PersistorManager.checkIfObjectExists = sinon.stub().resolves(true)
FileHandler.getFile(bucket, key, { format: 'png' }, (err, stream) => {
result = { err, stream }
done()
})
})
it('should not convert the file', function () {
expect(FileConverter.promises.convert).not.to.have.been.called
})
it('should not compress the converted file again', function () {
expect(ImageOptimiser.promises.compressPng).not.to.have.been.called
})
it('should return the cached stream', function () {
expect(result.err).not.to.exist
expect(result.stream).to.equal(sourceStream)
expect(PersistorManager.getObjectStream).to.have.been.calledWith(
bucket,
convertedKey
)
})
})
})
describe('when a style is defined', function () {
it('generates a thumbnail when requested', function (done) {
FileHandler.getFile(bucket, key, { style: 'thumbnail' }, err => {
expect(err).not.to.exist
expect(FileConverter.promises.thumbnail).to.have.been.called
expect(FileConverter.promises.preview).not.to.have.been.called
done()
})
})
it('generates a preview when requested', function (done) {
FileHandler.getFile(bucket, key, { style: 'preview' }, err => {
expect(err).not.to.exist
expect(FileConverter.promises.thumbnail).not.to.have.been.called
expect(FileConverter.promises.preview).to.have.been.called
done()
})
})
})
})
describe('getRedirectUrl', function () {
beforeEach(function () {
Settings.filestore = {
allowRedirects: true,
stores: {
userFiles: bucket,
},
}
})
it('should return a redirect url', function (done) {
FileHandler.getRedirectUrl(bucket, key, (err, url) => {
expect(err).not.to.exist
expect(url).to.equal(redirectUrl)
done()
})
})
it('should call the persistor to get a redirect url', function (done) {
FileHandler.getRedirectUrl(bucket, key, () => {
expect(PersistorManager.getRedirectUrl).to.have.been.calledWith(
bucket,
key
)
done()
})
})
it('should return null if options are supplied', function (done) {
FileHandler.getRedirectUrl(
bucket,
key,
{ start: 100, end: 200 },
(err, url) => {
expect(err).not.to.exist
expect(url).to.be.null
done()
}
)
})
it('should return null if the bucket is not one of the defined ones', function (done) {
FileHandler.getRedirectUrl('a_different_bucket', key, (err, url) => {
expect(err).not.to.exist
expect(url).to.be.null
done()
})
})
it('should return null if redirects are not enabled', function (done) {
Settings.filestore.allowRedirects = false
FileHandler.getRedirectUrl(bucket, key, (err, url) => {
expect(err).not.to.exist
expect(url).to.be.null
done()
})
})
})
describe('getDirectorySize', function () {
it('should call the filestore manager to get directory size', function (done) {
FileHandler.getDirectorySize(bucket, key, err => {
expect(err).not.to.exist
expect(PersistorManager.directorySize).to.have.been.calledWith(
bucket,
key
)
done()
})
})
})
})

View File

@@ -0,0 +1,67 @@
const sinon = require('sinon')
const chai = require('chai')
const { expect } = chai
const modulePath = '../../../app/js/ImageOptimiser.js'
const { FailedCommandError } = require('../../../app/js/Errors')
const SandboxedModule = require('sandboxed-module')
describe('ImageOptimiser', function () {
let ImageOptimiser, SafeExec
const sourcePath = '/wombat/potato.eps'
beforeEach(function () {
SafeExec = {
promises: sinon.stub().resolves(),
}
ImageOptimiser = SandboxedModule.require(modulePath, {
requires: {
'./SafeExec': SafeExec,
'@overleaf/metrics': {
Timer: sinon.stub().returns({ done: sinon.stub() }),
},
},
})
})
describe('compressPng', function () {
it('should convert the file', function (done) {
ImageOptimiser.compressPng(sourcePath, err => {
expect(err).not.to.exist
expect(SafeExec.promises).to.have.been.calledWith([
'optipng',
sourcePath,
])
done()
})
})
it('should return the error', function (done) {
SafeExec.promises.rejects('wombat herding failure')
ImageOptimiser.compressPng(sourcePath, err => {
expect(err.toString()).to.equal('wombat herding failure')
done()
})
})
})
describe('when optimiser is sigkilled', function () {
const expectedError = new FailedCommandError('', 'SIGKILL', '', '')
let error
beforeEach(function (done) {
SafeExec.promises.rejects(expectedError)
ImageOptimiser.compressPng(sourcePath, err => {
error = err
done()
})
})
it('should not produce an error', function () {
expect(error).not.to.exist
})
it('should log a warning', function () {
expect(this.logger.warn).to.have.been.calledOnce
})
})
})

View File

@@ -0,0 +1,37 @@
const SandboxedModule = require('sandboxed-module')
const modulePath = '../../../app/js/KeyBuilder.js'
describe('KeybuilderTests', function () {
let KeyBuilder
const key = 'wombat/potato'
beforeEach(function () {
KeyBuilder = SandboxedModule.require(modulePath, {
requires: { '@overleaf/settings': {} },
})
})
describe('cachedKey', function () {
it('should add the format to the key', function () {
const opts = { format: 'png' }
const newKey = KeyBuilder.addCachingToKey(key, opts)
newKey.should.equal(`${key}-converted-cache/format-png`)
})
it('should add the style to the key', function () {
const opts = { style: 'thumbnail' }
const newKey = KeyBuilder.addCachingToKey(key, opts)
newKey.should.equal(`${key}-converted-cache/style-thumbnail`)
})
it('should add format first, then style', function () {
const opts = {
style: 'thumbnail',
format: 'png',
}
const newKey = KeyBuilder.addCachingToKey(key, opts)
newKey.should.equal(`${key}-converted-cache/format-png-style-thumbnail`)
})
})
})

View File

@@ -0,0 +1,111 @@
const sinon = require('sinon')
const chai = require('chai')
const { expect } = chai
const modulePath = '../../../app/js/LocalFileWriter.js'
const SandboxedModule = require('sandboxed-module')
const { Errors } = require('@overleaf/object-persistor')
chai.use(require('sinon-chai'))
describe('LocalFileWriter', function () {
const writeStream = 'writeStream'
const readStream = 'readStream'
const settings = { path: { uploadFolder: '/uploads' } }
const fsPath = '/uploads/wombat'
const filename = 'wombat'
let stream, fs, LocalFileWriter
beforeEach(function () {
fs = {
createWriteStream: sinon.stub().returns(writeStream),
unlink: sinon.stub().yields(),
}
stream = {
pipeline: sinon.stub().yields(),
}
const ObjectPersistor = { Errors }
LocalFileWriter = SandboxedModule.require(modulePath, {
requires: {
fs,
stream,
'@overleaf/settings': settings,
'@overleaf/metrics': {
inc: sinon.stub(),
Timer: sinon.stub().returns({ done: sinon.stub() }),
},
'@overleaf/object-persistor': ObjectPersistor,
},
})
})
describe('writeStream', function () {
it('writes the stream to the upload folder', function (done) {
LocalFileWriter.writeStream(readStream, filename, (err, path) => {
expect(err).not.to.exist
expect(fs.createWriteStream).to.have.been.calledWith(fsPath)
expect(stream.pipeline).to.have.been.calledWith(readStream, writeStream)
expect(path).to.equal(fsPath)
done()
})
})
describe('when there is an error', function () {
const error = new Error('not enough ketchup')
beforeEach(function () {
stream.pipeline.yields(error)
})
it('should wrap the error', function () {
LocalFileWriter.writeStream(readStream, filename, err => {
expect(err).to.exist
expect(err.cause).to.equal(error)
})
})
it('should delete the temporary file', function () {
LocalFileWriter.writeStream(readStream, filename, () => {
expect(fs.unlink).to.have.been.calledWith(fsPath)
})
})
})
})
describe('deleteFile', function () {
it('should unlink the file', function (done) {
LocalFileWriter.deleteFile(fsPath, err => {
expect(err).not.to.exist
expect(fs.unlink).to.have.been.calledWith(fsPath)
done()
})
})
it('should not call unlink with an empty path', function (done) {
LocalFileWriter.deleteFile('', err => {
expect(err).not.to.exist
expect(fs.unlink).not.to.have.been.called
done()
})
})
it('should not throw a error if the file does not exist', function (done) {
const error = new Error('file not found')
error.code = 'ENOENT'
fs.unlink = sinon.stub().yields(error)
LocalFileWriter.deleteFile(fsPath, err => {
expect(err).not.to.exist
done()
})
})
it('should wrap the error', function (done) {
const error = new Error('failed to reticulate splines')
fs.unlink = sinon.stub().yields(error)
LocalFileWriter.deleteFile(fsPath, err => {
expect(err).to.exist
expect(err.cause).to.equal(error)
done()
})
})
})
})

View File

@@ -0,0 +1,110 @@
const chai = require('chai')
const should = chai.should()
const { expect } = chai
const modulePath = '../../../app/js/SafeExec'
const { Errors } = require('@overleaf/object-persistor')
const SandboxedModule = require('sandboxed-module')
describe('SafeExec', function () {
let settings, options, safeExec
beforeEach(function () {
settings = { enableConversions: true }
options = { timeout: 10 * 1000, killSignal: 'SIGTERM' }
const ObjectPersistor = { Errors }
safeExec = SandboxedModule.require(modulePath, {
globals: { process },
requires: {
'@overleaf/settings': settings,
'@overleaf/object-persistor': ObjectPersistor,
},
})
})
describe('safeExec', function () {
it('should execute a valid command', function (done) {
safeExec(['/bin/echo', 'hello'], options, (err, stdout, stderr) => {
stdout.should.equal('hello\n')
stderr.should.equal('')
should.not.exist(err)
done()
})
})
it('should error when conversions are disabled', function (done) {
settings.enableConversions = false
safeExec(['/bin/echo', 'hello'], options, err => {
expect(err).to.exist
done()
})
})
it('should execute a command with non-zero exit status', function (done) {
safeExec(['/usr/bin/env', 'false'], options, err => {
expect(err).to.exist
expect(err.name).to.equal('FailedCommandError')
expect(err.code).to.equal(1)
expect(err.stdout).to.equal('')
expect(err.stderr).to.equal('')
done()
})
})
it('should handle an invalid command', function (done) {
safeExec(['/bin/foobar'], options, err => {
err.code.should.equal('ENOENT')
done()
})
})
it('should handle a command that runs too long', function (done) {
safeExec(
['/bin/sleep', '10'],
{ timeout: 500, killSignal: 'SIGTERM' },
err => {
expect(err).to.exist
expect(err.name).to.equal('FailedCommandError')
expect(err.code).to.equal('SIGTERM')
done()
}
)
})
})
describe('as a promise', function () {
beforeEach(function () {
safeExec = safeExec.promises
})
it('should execute a valid command', async function () {
const { stdout, stderr } = await safeExec(['/bin/echo', 'hello'], options)
stdout.should.equal('hello\n')
stderr.should.equal('')
})
it('should throw a ConversionsDisabledError when appropriate', async function () {
settings.enableConversions = false
try {
await safeExec(['/bin/echo', 'hello'], options)
} catch (err) {
expect(err.name).to.equal('ConversionsDisabledError')
return
}
expect('method did not throw an error').not.to.exist
})
it('should throw a FailedCommandError when appropriate', async function () {
try {
await safeExec(['/usr/bin/env', 'false'], options)
} catch (err) {
expect(err.name).to.equal('FailedCommandError')
expect(err.code).to.equal(1)
return
}
expect('method did not throw an error').not.to.exist
})
})
})

View File

@@ -0,0 +1,21 @@
const chai = require('chai')
const { expect } = chai
const SandboxedModule = require('sandboxed-module')
describe('Settings', function () {
describe('s3', function () {
it('should use JSONified env var if present', function () {
const s3Settings = {
bucket1: {
auth_key: 'bucket1_key',
auth_secret: 'bucket1_secret',
},
}
process.env.S3_BUCKET_CREDENTIALS = JSON.stringify(s3Settings)
const settings = SandboxedModule.require('@overleaf/settings', {
globals: { console, process },
})
expect(settings.filestore.s3.bucketCreds).to.deep.equal(s3Settings)
})
})
})