2019-12-16 06:20:25 -05:00
|
|
|
/* 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.
|
2019-12-16 06:20:22 -05:00
|
|
|
/*
|
|
|
|
* 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",
|
2014-03-04 10:01:13 -05:00
|
|
|
key: "this_key"
|
2019-12-16 06:20:22 -05:00
|
|
|
},
|
|
|
|
stores: {
|
2014-03-04 10:01:13 -05:00
|
|
|
user_files:"sl_user_files"
|
2019-12-16 06:20:22 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
this.knoxClient = {
|
|
|
|
putFile:sinon.stub(),
|
|
|
|
copyFile:sinon.stub(),
|
|
|
|
list: sinon.stub(),
|
|
|
|
deleteMultiple: sinon.stub(),
|
2014-02-19 15:56:45 -05:00
|
|
|
get: sinon.stub()
|
2019-12-16 06:20:22 -05:00
|
|
|
};
|
|
|
|
this.knox =
|
|
|
|
{createClient: sinon.stub().returns(this.knoxClient)};
|
|
|
|
this.s3EventHandlers = {};
|
|
|
|
this.s3Request = {
|
|
|
|
on: sinon.stub().callsFake((event, callback) => {
|
|
|
|
return this.s3EventHandlers[event] = callback;
|
|
|
|
}),
|
2019-06-18 08:25:14 -04:00
|
|
|
send: sinon.stub()
|
2019-12-16 06:20:22 -05:00
|
|
|
};
|
|
|
|
this.s3Response = {
|
|
|
|
httpResponse: {
|
2019-06-19 12:58:17 -04:00
|
|
|
createUnbufferedStream: sinon.stub()
|
2019-12-16 06:20:22 -05:00
|
|
|
}
|
|
|
|
};
|
|
|
|
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(),
|
2014-02-14 11:39:05 -05:00
|
|
|
deleteFile: sinon.stub()
|
2019-12-16 06:20:22 -05:00
|
|
|
};
|
|
|
|
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,
|
2019-06-18 08:25:14 -04:00
|
|
|
auth_secret: secret
|
2019-12-16 06:20:22 -05:00
|
|
|
}
|
|
|
|
};
|
|
|
|
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,
|
2019-06-18 08:25:14 -04:00
|
|
|
secretAccessKey: secret
|
2019-12-16 06:20:22 -05:00
|
|
|
}
|
|
|
|
}]);
|
|
|
|
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(
|
2019-06-13 16:57:49 -04:00
|
|
|
"s3Client.headObject got unexpected arguments"
|
2019-12-16 06:20:22 -05:00
|
|
|
));
|
|
|
|
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();
|
|
|
|
});
|
|
|
|
}));
|
|
|
|
});
|