Files
overleaf-cep/services/web/test/unit/src/FileStore/FileStoreHandlerTests.js
Jakob Ackermann 63520c7076 Merge pull request #16859 from overleaf/jpa-sharelatex-cleanup
[misc] ShareLaTeX cleanup - high impact

GitOrigin-RevId: 6dcce9b0f15e30f7afcf6d69c3df36a369f38120
2024-02-09 09:04:11 +00:00

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()
}
)
})
})
})