first commit
This commit is contained in:
336
services/filestore/test/unit/js/FileControllerTests.js
Normal file
336
services/filestore/test/unit/js/FileControllerTests.js
Normal 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)
|
||||
})
|
||||
})
|
||||
})
|
107
services/filestore/test/unit/js/FileConverterTests.js
Normal file
107
services/filestore/test/unit/js/FileConverterTests.js
Normal 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]`)
|
||||
})
|
||||
})
|
||||
})
|
405
services/filestore/test/unit/js/FileHandlerTests.js
Normal file
405
services/filestore/test/unit/js/FileHandlerTests.js
Normal 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()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
67
services/filestore/test/unit/js/ImageOptimiserTests.js
Normal file
67
services/filestore/test/unit/js/ImageOptimiserTests.js
Normal 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
|
||||
})
|
||||
})
|
||||
})
|
37
services/filestore/test/unit/js/KeybuilderTests.js
Normal file
37
services/filestore/test/unit/js/KeybuilderTests.js
Normal 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`)
|
||||
})
|
||||
})
|
||||
})
|
111
services/filestore/test/unit/js/LocalFileWriterTests.js
Normal file
111
services/filestore/test/unit/js/LocalFileWriterTests.js
Normal 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()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
110
services/filestore/test/unit/js/SafeExecTests.js
Normal file
110
services/filestore/test/unit/js/SafeExecTests.js
Normal 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
|
||||
})
|
||||
})
|
||||
})
|
21
services/filestore/test/unit/js/SettingsTests.js
Normal file
21
services/filestore/test/unit/js/SettingsTests.js
Normal 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)
|
||||
})
|
||||
})
|
||||
})
|
Reference in New Issue
Block a user