| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517 |
- 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, cb) {
- if (type === 'response') {
- cb({ statusCode: 200 })
- }
- }
- }
- this.readStream = { my: 'readStream', on: sinon.stub() }
- this.request = sinon.stub()
- this.request.head = sinon.stub()
- this.filestoreUrl = 'http://filestore.sharelatex.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: {
- 'settings-sharelatex': 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, cb) {
- if (type === 'response') {
- cb({ 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()
- }
- )
- })
- })
- })
|