mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-05-24 17:51:51 +02:00
[misc] ShareLaTeX cleanup - high impact GitOrigin-RevId: 6dcce9b0f15e30f7afcf6d69c3df36a369f38120
518 lines
14 KiB
JavaScript
518 lines
14 KiB
JavaScript
const { assert, expect } = require('chai')
|
|
const sinon = require('sinon')
|
|
const SandboxedModule = require('sandboxed-module')
|
|
const Errors = require('../../../../app/src/Features/Errors/Errors')
|
|
const OError = require('@overleaf/o-error')
|
|
|
|
const MODULE_PATH = '../../../../app/src/Features/FileStore/FileStoreHandler.js'
|
|
|
|
describe('FileStoreHandler', function () {
|
|
beforeEach(function () {
|
|
this.fs = {
|
|
createReadStream: sinon.stub(),
|
|
lstat: sinon.stub().callsArgWith(1, null, {
|
|
isFile() {
|
|
return true
|
|
},
|
|
isDirectory() {
|
|
return false
|
|
},
|
|
}),
|
|
}
|
|
this.writeStream = {
|
|
my: 'writeStream',
|
|
on(type, fn) {
|
|
if (type === 'response') {
|
|
fn({ statusCode: 200 })
|
|
}
|
|
},
|
|
}
|
|
this.readStream = { my: 'readStream', on: sinon.stub() }
|
|
this.request = sinon.stub()
|
|
this.request.head = sinon.stub()
|
|
this.filestoreUrl = 'http://filestore.overleaf.test'
|
|
this.settings = {
|
|
apis: { filestore: { url: this.filestoreUrl } },
|
|
}
|
|
this.hashValue = '0123456789'
|
|
this.fileArgs = { name: 'upload-filename' }
|
|
this.fileId = 'file_id_here'
|
|
this.projectId = '1312312312'
|
|
this.fsPath = 'uploads/myfile.eps'
|
|
this.getFileUrl = (projectId, fileId) =>
|
|
`${this.filestoreUrl}/project/${projectId}/file/${fileId}`
|
|
this.getProjectUrl = projectId =>
|
|
`${this.filestoreUrl}/project/${projectId}`
|
|
this.FileModel = class File {
|
|
constructor(options) {
|
|
;({ name: this.name, hash: this.hash } = options)
|
|
this._id = 'file_id_here'
|
|
this.rev = 0
|
|
if (options.linkedFileData != null) {
|
|
this.linkedFileData = options.linkedFileData
|
|
}
|
|
}
|
|
}
|
|
this.FileHashManager = {
|
|
computeHash: sinon.stub().callsArgWith(1, null, this.hashValue),
|
|
}
|
|
this.handler = SandboxedModule.require(MODULE_PATH, {
|
|
requires: {
|
|
'@overleaf/settings': this.settings,
|
|
request: this.request,
|
|
'./FileHashManager': this.FileHashManager,
|
|
// FIXME: need to stub File object here
|
|
'../../models/File': {
|
|
File: this.FileModel,
|
|
},
|
|
fs: this.fs,
|
|
},
|
|
})
|
|
})
|
|
|
|
describe('uploadFileFromDisk', function () {
|
|
beforeEach(function () {
|
|
this.request.returns(this.writeStream)
|
|
})
|
|
|
|
it('should create read stream', function (done) {
|
|
this.fs.createReadStream.returns({
|
|
pipe() {},
|
|
on(type, cb) {
|
|
if (type === 'open') {
|
|
cb()
|
|
}
|
|
},
|
|
})
|
|
this.handler.uploadFileFromDisk(
|
|
this.projectId,
|
|
this.fileArgs,
|
|
this.fsPath,
|
|
() => {
|
|
this.fs.createReadStream.calledWith(this.fsPath).should.equal(true)
|
|
done()
|
|
}
|
|
)
|
|
})
|
|
|
|
it('should pipe the read stream to request', function (done) {
|
|
this.request.returns(this.writeStream)
|
|
this.fs.createReadStream.returns({
|
|
on(type, cb) {
|
|
if (type === 'open') {
|
|
cb()
|
|
}
|
|
},
|
|
pipe: o => {
|
|
this.writeStream.should.equal(o)
|
|
done()
|
|
},
|
|
})
|
|
this.handler.uploadFileFromDisk(
|
|
this.projectId,
|
|
this.fileArgs,
|
|
this.fsPath,
|
|
() => {}
|
|
)
|
|
})
|
|
|
|
it('should pass the correct options to request', function (done) {
|
|
const fileUrl = this.getFileUrl(this.projectId, this.fileId)
|
|
this.fs.createReadStream.returns({
|
|
pipe() {},
|
|
on(type, cb) {
|
|
if (type === 'open') {
|
|
cb()
|
|
}
|
|
},
|
|
})
|
|
this.handler.uploadFileFromDisk(
|
|
this.projectId,
|
|
this.fileArgs,
|
|
this.fsPath,
|
|
() => {
|
|
this.request.args[0][0].method.should.equal('post')
|
|
this.request.args[0][0].uri.should.equal(fileUrl)
|
|
done()
|
|
}
|
|
)
|
|
})
|
|
|
|
it('should callback with the url and fileRef', function (done) {
|
|
const fileUrl = this.getFileUrl(this.projectId, this.fileId)
|
|
this.fs.createReadStream.returns({
|
|
pipe() {},
|
|
on(type, cb) {
|
|
if (type === 'open') {
|
|
cb()
|
|
}
|
|
},
|
|
})
|
|
this.handler.uploadFileFromDisk(
|
|
this.projectId,
|
|
this.fileArgs,
|
|
this.fsPath,
|
|
(err, url, fileRef) => {
|
|
expect(err).to.not.exist
|
|
expect(url).to.equal(fileUrl)
|
|
expect(fileRef._id).to.equal(this.fileId)
|
|
expect(fileRef.hash).to.equal(this.hashValue)
|
|
done()
|
|
}
|
|
)
|
|
})
|
|
|
|
describe('symlink', function () {
|
|
it('should not read file if it is symlink', function (done) {
|
|
this.fs.lstat = sinon.stub().callsArgWith(1, null, {
|
|
isFile() {
|
|
return false
|
|
},
|
|
isDirectory() {
|
|
return false
|
|
},
|
|
})
|
|
|
|
this.handler.uploadFileFromDisk(
|
|
this.projectId,
|
|
this.fileArgs,
|
|
this.fsPath,
|
|
() => {
|
|
this.fs.createReadStream.called.should.equal(false)
|
|
done()
|
|
}
|
|
)
|
|
})
|
|
|
|
it('should not read file stat returns nothing', function (done) {
|
|
this.fs.lstat = sinon.stub().callsArgWith(1, null, null)
|
|
this.handler.uploadFileFromDisk(
|
|
this.projectId,
|
|
this.fileArgs,
|
|
this.fsPath,
|
|
() => {
|
|
this.fs.createReadStream.called.should.equal(false)
|
|
done()
|
|
}
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('when upload fails', function () {
|
|
beforeEach(function () {
|
|
this.writeStream.on = function (type, fn) {
|
|
if (type === 'response') {
|
|
fn({ statusCode: 500 })
|
|
}
|
|
}
|
|
})
|
|
|
|
it('should callback with an error', function (done) {
|
|
this.fs.createReadStream.callCount = 0
|
|
this.fs.createReadStream.returns({
|
|
pipe() {},
|
|
on(type, cb) {
|
|
if (type === 'open') {
|
|
cb()
|
|
}
|
|
},
|
|
})
|
|
this.handler.uploadFileFromDisk(
|
|
this.projectId,
|
|
this.fileArgs,
|
|
this.fsPath,
|
|
err => {
|
|
expect(err).to.exist
|
|
expect(err).to.be.instanceof(Error)
|
|
expect(this.fs.createReadStream.callCount).to.equal(
|
|
this.handler.RETRY_ATTEMPTS
|
|
)
|
|
done()
|
|
}
|
|
)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('deleteFile', function () {
|
|
it('should send a delete request to filestore api', function (done) {
|
|
const fileUrl = this.getFileUrl(this.projectId, this.fileId)
|
|
this.request.callsArgWith(1, null)
|
|
|
|
this.handler.deleteFile(this.projectId, this.fileId, err => {
|
|
assert.equal(err, undefined)
|
|
this.request.args[0][0].method.should.equal('delete')
|
|
this.request.args[0][0].uri.should.equal(fileUrl)
|
|
done()
|
|
})
|
|
})
|
|
|
|
it('should return the error if there is one', function (done) {
|
|
const error = 'my error'
|
|
this.request.callsArgWith(1, error)
|
|
this.handler.deleteFile(this.projectId, this.fileId, err => {
|
|
assert.equal(err, error)
|
|
done()
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('deleteProject', function () {
|
|
it('should send a delete request to filestore api', function (done) {
|
|
const projectUrl = this.getProjectUrl(this.projectId)
|
|
this.request.callsArgWith(1, null)
|
|
|
|
this.handler.deleteProject(this.projectId, err => {
|
|
assert.equal(err, undefined)
|
|
this.request.args[0][0].method.should.equal('delete')
|
|
this.request.args[0][0].uri.should.equal(projectUrl)
|
|
done()
|
|
})
|
|
})
|
|
|
|
it('should wrap the error if there is one', function (done) {
|
|
const error = new Error('my error')
|
|
this.request.callsArgWith(1, error)
|
|
this.handler.deleteProject(this.projectId, err => {
|
|
expect(OError.getFullStack(err)).to.match(
|
|
/something went wrong deleting a project in filestore/
|
|
)
|
|
expect(OError.getFullStack(err)).to.match(/my error/)
|
|
done()
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('getFileStream', function () {
|
|
beforeEach(function () {
|
|
this.query = {}
|
|
this.request.returns(this.readStream)
|
|
})
|
|
|
|
it('should get the stream with the correct params', function (done) {
|
|
const fileUrl = this.getFileUrl(this.projectId, this.fileId)
|
|
this.handler.getFileStream(
|
|
this.projectId,
|
|
this.fileId,
|
|
this.query,
|
|
(err, stream) => {
|
|
if (err) {
|
|
return done(err)
|
|
}
|
|
this.request.args[0][0].method.should.equal('get')
|
|
this.request.args[0][0].uri.should.equal(fileUrl)
|
|
done()
|
|
}
|
|
)
|
|
})
|
|
|
|
it('should get stream from request', function (done) {
|
|
this.handler.getFileStream(
|
|
this.projectId,
|
|
this.fileId,
|
|
this.query,
|
|
(err, stream) => {
|
|
if (err) {
|
|
return done(err)
|
|
}
|
|
stream.should.equal(this.readStream)
|
|
done()
|
|
}
|
|
)
|
|
})
|
|
|
|
it('should add an error handler', function (done) {
|
|
this.handler.getFileStream(
|
|
this.projectId,
|
|
this.fileId,
|
|
this.query,
|
|
(err, stream) => {
|
|
if (err) {
|
|
return done(err)
|
|
}
|
|
stream.on.calledWith('error').should.equal(true)
|
|
done()
|
|
}
|
|
)
|
|
})
|
|
|
|
describe('when range is specified in query', function () {
|
|
beforeEach(function () {
|
|
this.query = { range: '0-10' }
|
|
})
|
|
|
|
it('should add a range header', function (done) {
|
|
this.handler.getFileStream(
|
|
this.projectId,
|
|
this.fileId,
|
|
this.query,
|
|
(err, stream) => {
|
|
if (err) {
|
|
return done(err)
|
|
}
|
|
this.request.callCount.should.equal(1)
|
|
const { headers } = this.request.firstCall.args[0]
|
|
expect(headers).to.have.keys('range')
|
|
expect(headers.range).to.equal('bytes=0-10')
|
|
done()
|
|
}
|
|
)
|
|
})
|
|
|
|
describe('when range is invalid', function () {
|
|
;['0-', '-100', 'one-two', 'nonsense'].forEach(r => {
|
|
beforeEach(function () {
|
|
this.query = { range: `${r}` }
|
|
})
|
|
|
|
it(`should not add a range header for '${r}'`, function (done) {
|
|
this.handler.getFileStream(
|
|
this.projectId,
|
|
this.fileId,
|
|
this.query,
|
|
(err, stream) => {
|
|
if (err) {
|
|
return done(err)
|
|
}
|
|
this.request.callCount.should.equal(1)
|
|
const { headers } = this.request.firstCall.args[0]
|
|
expect(headers).to.not.have.keys('range')
|
|
done()
|
|
}
|
|
)
|
|
})
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('getFileSize', function () {
|
|
it('returns the file size reported by filestore', function (done) {
|
|
const expectedFileSize = 32432
|
|
const fileUrl = this.getFileUrl(this.projectId, this.fileId)
|
|
this.request.head.yields(
|
|
new Error('request.head() received unexpected arguments')
|
|
)
|
|
this.request.head.withArgs(fileUrl).yields(null, {
|
|
statusCode: 200,
|
|
headers: {
|
|
'content-length': expectedFileSize,
|
|
},
|
|
})
|
|
|
|
this.handler.getFileSize(this.projectId, this.fileId, (err, fileSize) => {
|
|
if (err) {
|
|
return done(err)
|
|
}
|
|
expect(fileSize).to.equal(expectedFileSize)
|
|
done()
|
|
})
|
|
})
|
|
|
|
it('throws a NotFoundError on a 404 from filestore', function (done) {
|
|
this.request.head.yields(null, { statusCode: 404 })
|
|
|
|
this.handler.getFileSize(this.projectId, this.fileId, err => {
|
|
expect(err).to.be.instanceof(Errors.NotFoundError)
|
|
done()
|
|
})
|
|
})
|
|
|
|
it('throws an error on a non-200 from filestore', function (done) {
|
|
this.request.head.yields(null, { statusCode: 500 })
|
|
|
|
this.handler.getFileSize(this.projectId, this.fileId, err => {
|
|
expect(err).to.be.instanceof(Error)
|
|
done()
|
|
})
|
|
})
|
|
|
|
it('rethrows errors from filestore', function (done) {
|
|
this.request.head.yields(new Error())
|
|
|
|
this.handler.getFileSize(this.projectId, this.fileId, err => {
|
|
expect(err).to.be.instanceof(Error)
|
|
done()
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('copyFile', function () {
|
|
beforeEach(function () {
|
|
this.newProjectId = 'new project'
|
|
this.newFileId = 'new file id'
|
|
})
|
|
|
|
it('should post json', function (done) {
|
|
const newFileUrl = this.getFileUrl(this.newProjectId, this.newFileId)
|
|
this.request.callsArgWith(1, null, { statusCode: 200 })
|
|
|
|
this.handler.copyFile(
|
|
this.projectId,
|
|
this.fileId,
|
|
this.newProjectId,
|
|
this.newFileId,
|
|
() => {
|
|
this.request.args[0][0].method.should.equal('put')
|
|
this.request.args[0][0].uri.should.equal(newFileUrl)
|
|
this.request.args[0][0].json.source.project_id.should.equal(
|
|
this.projectId
|
|
)
|
|
this.request.args[0][0].json.source.file_id.should.equal(this.fileId)
|
|
done()
|
|
}
|
|
)
|
|
})
|
|
|
|
it('returns the url', function (done) {
|
|
const expectedUrl = this.getFileUrl(this.newProjectId, this.newFileId)
|
|
this.request.callsArgWith(1, null, { statusCode: 200 })
|
|
this.handler.copyFile(
|
|
this.projectId,
|
|
this.fileId,
|
|
this.newProjectId,
|
|
this.newFileId,
|
|
(err, url) => {
|
|
if (err) {
|
|
return done(err)
|
|
}
|
|
url.should.equal(expectedUrl)
|
|
done()
|
|
}
|
|
)
|
|
})
|
|
|
|
it('should return the err', function (done) {
|
|
const error = new Error('error')
|
|
this.request.callsArgWith(1, error)
|
|
this.handler.copyFile(
|
|
this.projectId,
|
|
this.fileId,
|
|
this.newProjectId,
|
|
this.newFileId,
|
|
err => {
|
|
err.should.equal(error)
|
|
done()
|
|
}
|
|
)
|
|
})
|
|
|
|
it('should return an error for a non-success statusCode', function (done) {
|
|
this.request.callsArgWith(1, null, { statusCode: 500 })
|
|
this.handler.copyFile(
|
|
this.projectId,
|
|
this.fileId,
|
|
this.newProjectId,
|
|
this.newFileId,
|
|
err => {
|
|
err.should.be.an('error')
|
|
err.message.should.equal(
|
|
'non-ok response from filestore for copyFile: 500'
|
|
)
|
|
done()
|
|
}
|
|
)
|
|
})
|
|
})
|
|
})
|