/* eslint-disable handle-callback-err, no-return-assign, no-unused-vars, */ // TODO: This file was created by bulk-decaffeinate. // Fix any style issues and re-enable lint. /* * decaffeinate suggestions: * DS102: Remove unnecessary code created because of implicit returns * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ const { assert } = require('chai') const sinon = require('sinon') const chai = require('chai') const should = chai.should() const { expect } = chai const modulePath = '../../../app/js/S3PersistorManager.js' const SandboxedModule = require('sandboxed-module') describe('S3PersistorManagerTests', function() { beforeEach(function() { this.settings = { filestore: { backend: 's3', s3: { secret: 'secret', key: 'this_key' }, stores: { user_files: 'sl_user_files' } } } this.knoxClient = { putFile: sinon.stub(), copyFile: sinon.stub(), list: sinon.stub(), deleteMultiple: sinon.stub(), get: sinon.stub() } this.knox = { createClient: sinon.stub().returns(this.knoxClient) } this.s3EventHandlers = {} this.s3Request = { on: sinon.stub().callsFake((event, callback) => { return (this.s3EventHandlers[event] = callback) }), send: sinon.stub() } this.s3Response = { httpResponse: { createUnbufferedStream: sinon.stub() } } this.s3Client = { copyObject: sinon.stub(), headObject: sinon.stub(), getObject: sinon.stub().returns(this.s3Request) } this.awsS3 = sinon.stub().returns(this.s3Client) this.LocalFileWriter = { writeStream: sinon.stub(), deleteFile: sinon.stub() } this.request = sinon.stub() this.requires = { knox: this.knox, 'aws-sdk/clients/s3': this.awsS3, 'settings-sharelatex': this.settings, './LocalFileWriter': this.LocalFileWriter, 'logger-sharelatex': { log() {}, err() {} }, request: this.request, './Errors': (this.Errors = { NotFoundError: sinon.stub() }) } this.key = 'my/key' this.bucketName = 'my-bucket' this.error = 'my errror' return (this.S3PersistorManager = SandboxedModule.require(modulePath, { requires: this.requires })) }) describe('getFileStream', function() { describe('success', function() { beforeEach(function() { this.expectedStream = { expectedStream: true } this.expectedStream.on = sinon.stub() this.s3Request.send.callsFake(() => { return this.s3EventHandlers.httpHeaders( 200, {}, this.s3Response, 'OK' ) }) return this.s3Response.httpResponse.createUnbufferedStream.returns( this.expectedStream ) }) it('returns a stream', function(done) { return this.S3PersistorManager.getFileStream( this.bucketName, this.key, {}, (err, stream) => { if (err != null) { return done(err) } expect(stream).to.equal(this.expectedStream) return done() } ) }) it('sets the AWS client up with credentials from settings', function(done) { return this.S3PersistorManager.getFileStream( this.bucketName, this.key, {}, (err, stream) => { if (err != null) { return done(err) } expect(this.awsS3.lastCall.args).to.deep.equal([ { credentials: { accessKeyId: this.settings.filestore.s3.key, secretAccessKey: this.settings.filestore.s3.secret } } ]) return done() } ) }) it('fetches the right key from the right bucket', function(done) { return this.S3PersistorManager.getFileStream( this.bucketName, this.key, {}, (err, stream) => { if (err != null) { return done(err) } expect(this.s3Client.getObject.lastCall.args).to.deep.equal([ { Bucket: this.bucketName, Key: this.key } ]) return done() } ) }) it('accepts alternative credentials', function(done) { const accessKeyId = 'that_key' const secret = 'that_secret' const opts = { credentials: { auth_key: accessKeyId, auth_secret: secret } } return this.S3PersistorManager.getFileStream( this.bucketName, this.key, opts, (err, stream) => { if (err != null) { return done(err) } expect(this.awsS3.lastCall.args).to.deep.equal([ { credentials: { accessKeyId, secretAccessKey: secret } } ]) expect(stream).to.equal(this.expectedStream) return done() } ) }) return it('accepts byte range', function(done) { const start = 0 const end = 8 const opts = { start, end } return this.S3PersistorManager.getFileStream( this.bucketName, this.key, opts, (err, stream) => { if (err != null) { return done(err) } expect(this.s3Client.getObject.lastCall.args).to.deep.equal([ { Bucket: this.bucketName, Key: this.key, Range: `bytes=${start}-${end}` } ]) expect(stream).to.equal(this.expectedStream) return done() } ) }) }) return describe('errors', function() { describe("when the file doesn't exist", function() { beforeEach(function() { return this.s3Request.send.callsFake(() => { return this.s3EventHandlers.httpHeaders( 404, {}, this.s3Response, 'Not found' ) }) }) return it('returns a NotFoundError that indicates the bucket and key', function(done) { return this.S3PersistorManager.getFileStream( this.bucketName, this.key, {}, (err, stream) => { expect(err).to.be.instanceof(this.Errors.NotFoundError) const errMsg = this.Errors.NotFoundError.lastCall.args[0] expect(errMsg).to.match(new RegExp(`.*${this.bucketName}.*`)) expect(errMsg).to.match(new RegExp(`.*${this.key}.*`)) return done() } ) }) }) describe('when S3 encounters an unkown error', function() { beforeEach(function() { return this.s3Request.send.callsFake(() => { return this.s3EventHandlers.httpHeaders( 500, {}, this.s3Response, 'Internal server error' ) }) }) return it('returns an error', function(done) { return this.S3PersistorManager.getFileStream( this.bucketName, this.key, {}, (err, stream) => { expect(err).to.be.instanceof(Error) return done() } ) }) }) return describe('when the S3 request errors out before receiving HTTP headers', function() { beforeEach(function() { return this.s3Request.send.callsFake(() => { return this.s3EventHandlers.error(new Error('connection failed')) }) }) return it('returns an error', function(done) { return this.S3PersistorManager.getFileStream( this.bucketName, this.key, {}, (err, stream) => { expect(err).to.be.instanceof(Error) return done() } ) }) }) }) }) describe('getFileSize', function() { it('should obtain the file size from S3', function(done) { const expectedFileSize = 123 this.s3Client.headObject.yields( new Error('s3Client.headObject got unexpected arguments') ) this.s3Client.headObject .withArgs({ Bucket: this.bucketName, Key: this.key }) .yields(null, { ContentLength: expectedFileSize }) return this.S3PersistorManager.getFileSize( this.bucketName, this.key, (err, fileSize) => { if (err != null) { return done(err) } expect(fileSize).to.equal(expectedFileSize) return done() } ) }) ;[403, 404].forEach(statusCode => it(`should throw NotFoundError when S3 responds with ${statusCode}`, function(done) { const error = new Error() error.statusCode = statusCode this.s3Client.headObject.yields(error) return this.S3PersistorManager.getFileSize( this.bucketName, this.key, (err, fileSize) => { expect(err).to.be.an.instanceof(this.Errors.NotFoundError) return done() } ) }) ) return it('should rethrow any other error', function(done) { const error = new Error() this.s3Client.headObject.yields(error) this.s3Client.headObject.yields(error) return this.S3PersistorManager.getFileSize( this.bucketName, this.key, (err, fileSize) => { expect(err).to.equal(error) return done() } ) }) }) describe('sendFile', function() { beforeEach(function() { return this.knoxClient.putFile.returns({ on() {} }) }) it('should put file with knox', function(done) { this.LocalFileWriter.deleteFile.callsArgWith(1) this.knoxClient.putFile.callsArgWith(2, this.error) return this.S3PersistorManager.sendFile( this.bucketName, this.key, this.fsPath, err => { this.knoxClient.putFile .calledWith(this.fsPath, this.key) .should.equal(true) err.should.equal(this.error) return done() } ) }) return it('should delete the file and pass the error with it', function(done) { this.LocalFileWriter.deleteFile.callsArgWith(1) this.knoxClient.putFile.callsArgWith(2, this.error) return this.S3PersistorManager.sendFile( this.bucketName, this.key, this.fsPath, err => { this.knoxClient.putFile .calledWith(this.fsPath, this.key) .should.equal(true) err.should.equal(this.error) return done() } ) }) }) describe('sendStream', function() { beforeEach(function() { this.fsPath = 'to/some/where' this.origin = { on() {} } return (this.S3PersistorManager.sendFile = sinon.stub().callsArgWith(3)) }) it('should send stream to LocalFileWriter', function(done) { this.LocalFileWriter.deleteFile.callsArgWith(1) this.LocalFileWriter.writeStream.callsArgWith(2, null, this.fsPath) return this.S3PersistorManager.sendStream( this.bucketName, this.key, this.origin, () => { this.LocalFileWriter.writeStream .calledWith(this.origin) .should.equal(true) return done() } ) }) it('should return the error from LocalFileWriter', function(done) { this.LocalFileWriter.deleteFile.callsArgWith(1) this.LocalFileWriter.writeStream.callsArgWith(2, this.error) return this.S3PersistorManager.sendStream( this.bucketName, this.key, this.origin, err => { err.should.equal(this.error) return done() } ) }) return it('should send the file to the filestore', function(done) { this.LocalFileWriter.deleteFile.callsArgWith(1) this.LocalFileWriter.writeStream.callsArgWith(2) return this.S3PersistorManager.sendStream( this.bucketName, this.key, this.origin, err => { this.S3PersistorManager.sendFile.called.should.equal(true) return done() } ) }) }) describe('copyFile', function() { beforeEach(function() { this.sourceKey = 'my/key' return (this.destKey = 'my/dest/key') }) it('should use AWS SDK to copy file', function(done) { this.s3Client.copyObject.callsArgWith(1, this.error) return this.S3PersistorManager.copyFile( this.bucketName, this.sourceKey, this.destKey, err => { err.should.equal(this.error) this.s3Client.copyObject .calledWith({ Bucket: this.bucketName, Key: this.destKey, CopySource: this.bucketName + '/' + this.key }) .should.equal(true) return done() } ) }) return it('should return a NotFoundError object if the original file does not exist', function(done) { const NoSuchKeyError = { code: 'NoSuchKey' } this.s3Client.copyObject.callsArgWith(1, NoSuchKeyError) return this.S3PersistorManager.copyFile( this.bucketName, this.sourceKey, this.destKey, err => { expect(err instanceof this.Errors.NotFoundError).to.equal(true) return done() } ) }) }) describe('deleteDirectory', () => it('should list the contents passing them onto multi delete', function(done) { const data = { Contents: [{ Key: '1234' }, { Key: '456' }] } this.knoxClient.list.callsArgWith(1, null, data) this.knoxClient.deleteMultiple.callsArgWith(1) return this.S3PersistorManager.deleteDirectory( this.bucketName, this.key, err => { this.knoxClient.deleteMultiple .calledWith(['1234', '456']) .should.equal(true) return done() } ) })) describe('deleteFile', function() { it('should use correct options', function(done) { this.request.callsArgWith(1) return this.S3PersistorManager.deleteFile( this.bucketName, this.key, err => { const opts = this.request.args[0][0] assert.deepEqual(opts.aws, { key: this.settings.filestore.s3.key, secret: this.settings.filestore.s3.secret, bucket: this.bucketName }) opts.method.should.equal('delete') opts.timeout.should.equal(30 * 1000) opts.uri.should.equal( `https://${this.bucketName}.s3.amazonaws.com/${this.key}` ) return done() } ) }) return it('should return the error', function(done) { this.request.callsArgWith(1, this.error) return this.S3PersistorManager.deleteFile( this.bucketName, this.key, err => { err.should.equal(this.error) return done() } ) }) }) describe('checkIfFileExists', function() { it('should use correct options', function(done) { this.request.callsArgWith(1, null, { statusCode: 200 }) return this.S3PersistorManager.checkIfFileExists( this.bucketName, this.key, err => { const opts = this.request.args[0][0] assert.deepEqual(opts.aws, { key: this.settings.filestore.s3.key, secret: this.settings.filestore.s3.secret, bucket: this.bucketName }) opts.method.should.equal('head') opts.timeout.should.equal(30 * 1000) opts.uri.should.equal( `https://${this.bucketName}.s3.amazonaws.com/${this.key}` ) return done() } ) }) it('should return true for a 200', function(done) { this.request.callsArgWith(1, null, { statusCode: 200 }) return this.S3PersistorManager.checkIfFileExists( this.bucketName, this.key, (err, exists) => { exists.should.equal(true) return done() } ) }) it('should return false for a non 200', function(done) { this.request.callsArgWith(1, null, { statusCode: 404 }) return this.S3PersistorManager.checkIfFileExists( this.bucketName, this.key, (err, exists) => { exists.should.equal(false) return done() } ) }) return it('should return the error', function(done) { this.request.callsArgWith(1, this.error, {}) return this.S3PersistorManager.checkIfFileExists( this.bucketName, this.key, err => { err.should.equal(this.error) return done() } ) }) }) return describe('directorySize', () => it('should sum directory files size', function(done) { const data = { Contents: [{ Size: 1024 }, { Size: 2048 }] } this.knoxClient.list.callsArgWith(1, null, data) return this.S3PersistorManager.directorySize( this.bucketName, this.key, (err, totalSize) => { totalSize.should.equal(3072) return done() } ) })) })