hapiest-s3-storage
Version:
A simple s3 storage service that's flexible and allows mocking of s3 locally
548 lines (484 loc) • 21 kB
JavaScript
'use strict';
const Should = require('should');
const Promise = require('bluebird');
const Path = require('path');
const Fs = require('fs');
const rmdir = require('rmdir');
const Sinon = require('sinon');
const NodeConfig = require('config');
const S3StorageServiceFactory = require('../../lib/s3StorageServiceFactory');
const LoggerConfigFactory = require('hapiest-logger/lib/loggerConfigFactory');
const LoggerFactory = require('hapiest-logger/lib/loggerFactory');
const loggerConfig = LoggerConfigFactory.createFromJsObj({enabled: false});
const logger = LoggerFactory.createLogger(loggerConfig);
const basePath = Path.join(__dirname, '../unit-helper/s3StorageService/localstorage');
const testBucket1 = 'somebucket';
const testBucket2 = 'anotherbucket';
const s3ServiceLocalStorageWithBucket = S3StorageServiceFactory.create({
type: 'localstorage',
bucket: testBucket1,
localConfig: {
path: './'
}
}, logger, basePath);
const s3ServiceLocalStorageWithoutBucket = S3StorageServiceFactory.create({
type: 'localstorage',
localConfig: {
path: './'
}
}, logger, basePath);
const s3ServiceLocalStorageWithBucketAndKeyPrefix = S3StorageServiceFactory.create({
type: 'localstorage',
bucket: testBucket1,
keyPrefix: 'tmp/blah/hello',
localConfig: {
path: './'
}
}, logger, basePath);
const s3ServiceWithBucket = S3StorageServiceFactory.create({
type: 's3',
bucket: 'vizualai-test',
s3Config: {
awsAccessKey: NodeConfig.get('aws.awsAccessKey'),
awsSecretKey: NodeConfig.get('aws.awsSecretKey'),
httpTimeoutMs: 15000
}
});
const s3ServiceWithBaseUrlBucketAndVirutalHostUrlStyle = S3StorageServiceFactory.create({
type: 's3',
bucket: 'vizualai-test',
urlStyle: 'virtualHost',
keyPrefix: 'images/tmp',
s3Config: {
awsAccessKey: NodeConfig.get('aws.awsAccessKey'),
awsSecretKey: NodeConfig.get('aws.awsSecretKey'),
baseUrl: 'http://s3.docker',
httpTimeoutMs: 15000
}
});
const s3ServiceWithBucketAndKeyPrefix = S3StorageServiceFactory.create({
type: 's3',
bucket: 'vizualai-test',
keyPrefix: 'images/tmp',
s3Config: {
awsAccessKey: NodeConfig.get('aws.awsAccessKey'),
awsSecretKey: NodeConfig.get('aws.awsSecretKey')
}
});
const s3ServiceForceTimeout = S3StorageServiceFactory.create({
type: 's3',
bucket: 'vizualai-test',
keyPrefix: 'images/tmp',
s3Config: {
awsAccessKey: NodeConfig.get('aws.awsAccessKey'),
awsSecretKey: NodeConfig.get('aws.awsSecretKey'),
httpTimeoutMs: 1
}
});
const s3ServiceReadonly = S3StorageServiceFactory.create({
type: 's3',
bucket: 'vizualai-test',
keyPrefix: 'images/tmp',
readOnly: true,
s3Config: {
awsAccessKey: NodeConfig.get('aws.awsAccessKey'),
awsSecretKey: NodeConfig.get('aws.awsSecretKey')
}
});
const testFilePath = Path.join(__dirname,'../unit-helper/s3StorageService/testSources/sample.txt');
const testFileBody = Fs.readFileSync(testFilePath);
describe('S3StorageService', function() {
describe('localstorage', function() {
beforeEach(function(done) {
rmdir(basePath, () => done());
});
after(function(done) {
rmdir(basePath, () => done());
});
it('Should put an object and then get the object with the same key', function () {
const testKey = 'some/key/up/there.txt';
return s3ServiceLocalStorageWithBucket.putObject({
Key: testKey,
Body: testFileBody
})
.then((results) => {
(Fs.existsSync(Path.join(basePath, testBucket1, testKey))).should.be.True();
return s3ServiceLocalStorageWithBucket.getObject({
Key: testKey
});
})
.then(result => {
(result.Key).should.eql(testKey);
(result.Body).should.eql(testFileBody);
});
});
it('Should put an object and then get the object with the same key by providing the bucket in individual functions', function () {
const testKey = 'blah.txt';
return s3ServiceLocalStorageWithoutBucket.putObject({
Bucket: testBucket2,
Key: testKey,
Body: testFileBody
})
.then((results) => {
(Fs.existsSync(Path.join(basePath, testBucket2, testKey))).should.be.True();
return s3ServiceLocalStorageWithoutBucket.getObject({
Bucket: testBucket2,
Key: testKey
});
})
.then(result => {
(result.Key).should.eql(testKey);
(result.Body).should.eql(testFileBody);
});
});
it('Should delete an object', function () {
const testKey = 'some/key/up/there.txt';
return s3ServiceLocalStorageWithBucket.putObject({
Key: testKey,
Body: testFileBody
})
.then((results) => {
(Fs.existsSync(Path.join(basePath, testBucket1, testKey))).should.be.True();
})
.then(() => s3ServiceLocalStorageWithBucket.deleteObject({Key: testKey}))
.then(() => s3ServiceLocalStorageWithBucket.getObject({Key: testKey}))
.then(() => {
throw new Error('Should fail with no such file error (err.code === ENOENT)')
}, err => {
Should.exist(err);
err.code.should.eql('ENOENT')
});
});
it('Should upload an object and then get the object with the same key', function () {
var testKey = 'myUploadKey';
return s3ServiceLocalStorageWithBucket.upload({
Key: testKey,
Bucket: testBucket1,
Body: testFileBody
}, {})
.then(function (result) {
(Fs.existsSync(Path.join(basePath, testBucket1, testKey))).should.be.equal(true);
return s3ServiceLocalStorageWithBucket.getObject({
Key: testKey,
Bucket: testBucket1
})
})
.then(function (result) {
(result.Key).should.eql(testKey);
(result.Body).should.eql(testFileBody);
});
});
it('Should put an object with "short" key and get the object with both the "short" and full key', function() {
var testKey = 'myPutKey';
return s3ServiceLocalStorageWithBucketAndKeyPrefix.putObject({
Key: testKey,
Body: testFileBody
})
.then(results => {
return Promise.all([
s3ServiceLocalStorageWithBucketAndKeyPrefix.getObject({Key: testKey}),
s3ServiceLocalStorageWithBucketAndKeyPrefix.getObject({Key: `tmp/blah/hello/${testKey}`})
]);
})
.spread((resultShortKey, resultLongKey) => {
resultShortKey.Key.should.eql(`tmp/blah/hello/${testKey}`);
resultShortKey.Body.should.eql(testFileBody);
resultLongKey.Key.should.eql(`tmp/blah/hello/${testKey}`);
resultLongKey.Body.should.eql(testFileBody);
})
;
});
});
describe('s3', function() {
this.timeout(10000);
it('Should put an object and then get the object with the same key', function () {
const testKey = 'some/key/up/there.txt';
return s3ServiceWithBucket.putObject({
Key: testKey,
Body: testFileBody
})
.then((results) => {
return s3ServiceWithBucket.getObject({
Key: testKey
});
})
.then(result => {
(result.Key).should.eql(testKey);
(result.Body).should.eql(testFileBody);
});
});
it('Should delete an object', function () {
const testKey = 'some/key/up/there.txt';
return s3ServiceWithBucket.putObject({
Key: testKey,
Body: testFileBody
})
.then(() => s3ServiceWithBucket.getObject({Key: testKey}))
.then(result => {
(result.Key).should.eql(testKey);
(result.Body).should.eql(testFileBody);
})
.then(() => s3ServiceWithBucket.deleteObject({Key: testKey}))
.then(() => s3ServiceWithBucket.getObject({Key: testKey}))
.then(() => {
throw new Error('Should fail with no such file error (err.code === NoSuchKey)')
}, err => {
Should.exist(err);
err.code.should.eql('NoSuchKey');
});
});
it('Should delete objects with same prefix', function () {
const testKey1 = 'some/key/up/there1.txt';
const testKey2 = 'some/key/up/there2.txt';
return s3ServiceWithBucket.putObject({
Key: testKey1,
Body: testFileBody
})
.then(() => s3ServiceWithBucket.getObject({Key: testKey1}))
.then(result => {
(result.Key).should.eql(testKey1);
(result.Body).should.eql(testFileBody);
})
.then(() => s3ServiceWithBucket.putObject({
Key: testKey2,
Body: testFileBody
}))
.then(() => s3ServiceWithBucket.getObject({Key: testKey2}))
.then(result => {
(result.Key).should.eql(testKey2);
(result.Body).should.eql(testFileBody);
})
.then(() => s3ServiceWithBucket.deleteObjectsWithPrefix({Prefix: 'some/key/up'}))
.then(() => s3ServiceWithBucket.listObjects({Prefix: 'some/key/up'}))
.then(results => {
results.Contents.length.should.equal(0);
})
});
it('Should upload an object and then get the object with the same key', function () {
var testKey = 'myUploadKey';
return s3ServiceWithBucket.upload({
Key: testKey,
Body: testFileBody
})
.then(result =>{
return s3ServiceWithBucket.getObject({
Key: testKey
})
})
.then(result => {
(result.Key).should.eql(testKey);
(result.Body).should.eql(testFileBody);
})
});
it('Should put an object with "short" key and get the object with both the "short" and full key', function() {
var testKey = 'myPutKey';
return s3ServiceWithBucketAndKeyPrefix.putObject({
Key: testKey,
Body: testFileBody
})
.then(results => {
return Promise.all([
s3ServiceWithBucketAndKeyPrefix.getObject({Key: testKey}),
s3ServiceWithBucketAndKeyPrefix.getObject({Key: `images/tmp/${testKey}`})
]);
})
.spread((resultShortKey, resultLongKey) => {
resultShortKey.Key.should.eql(`images/tmp/${testKey}`);
resultShortKey.Body.should.eql(testFileBody);
resultLongKey.Key.should.eql(`images/tmp/${testKey}`);
resultLongKey.Body.should.eql(testFileBody);
})
;
});
it('Should retry putObject when maxRetriesOnTimeout > 0', function() {
const testKey = 'myPutKey';
let timesCalled = 0;
const requestTimeoutErr = new Error('RequestTimeout');
requestTimeoutErr.code = 'RequestTimeout';
const _putObjectSimpleStub = Sinon.stub(s3ServiceWithBucketAndKeyPrefix, '_putObjectSimple', function(params) {
timesCalled += 1;
if (timesCalled === 1 || timesCalled === 2) {
return Promise.reject(requestTimeoutErr);
} else {
_putObjectSimpleStub.restore();
return s3ServiceWithBucketAndKeyPrefix._putObjectSimple(params);
}
});
const _putObjectRecursiveSpy = Sinon.spy(s3ServiceWithBucketAndKeyPrefix, '_putObjectRecursive');
return Promise.resolve()
.then(() => s3ServiceWithBucketAndKeyPrefix.deleteObject({Key: testKey}))
.then(() => s3ServiceWithBucketAndKeyPrefix.putObject({
Key: testKey,
Body: testFileBody
}))
.then(results => s3ServiceWithBucketAndKeyPrefix.getObject({Key: testKey}))
.then(result => {
result.Key.should.eql(`images/tmp/${testKey}`);
result.Body.should.eql(testFileBody);
_putObjectRecursiveSpy.calledThrice.should.be.True();
})
.finally(() => {
_putObjectSimpleStub.restore();
_putObjectRecursiveSpy.restore();
})
;
});
it('Should throw an error if it hits too many RequestTimeout errors', function() {
const testKey = 'myPutKey';
let timesCalled = 0;
const requestTimeoutErr = new Error('RequestTimeout');
requestTimeoutErr.code = 'RequestTimeout';
const _putObjectSimpleStub = Sinon.stub(s3ServiceWithBucketAndKeyPrefix, '_putObjectSimple', function(params) {
timesCalled += 1;
if (timesCalled <= 6) {
return Promise.reject(requestTimeoutErr);
} else {
_putObjectSimpleStub.restore();
return s3ServiceWithBucketAndKeyPrefix._putObjectSimple(params);
}
});
const _putObjectRecursiveSpy = Sinon.spy(s3ServiceWithBucketAndKeyPrefix, '_putObjectRecursive');
let expectedError;
return Promise.resolve()
.then(() => s3ServiceWithBucketAndKeyPrefix.deleteObject({Key: testKey}))
.then(() => s3ServiceWithBucketAndKeyPrefix.putObject({
Key: testKey,
Body: testFileBody
}))
.catch(err => expectedError = err)
.then(() => {
Should.exist(expectedError);
expectedError.code.should.eql('MaxRetriesReached');
expectedError.message.should.eql('S3StorageService: max retries (5) on timeout reached. Search for AWS SDK S3 RequestTimeout errors and how to resolve them.');
_putObjectRecursiveSpy.callCount.should.eql(6);
})
.finally(() => {
_putObjectSimpleStub.restore();
_putObjectRecursiveSpy.restore();
})
;
});
it('Should timeout when uploading', function () {
const testKey = 'myUploadKey';
var expectedError;
return s3ServiceForceTimeout.upload({
Key: testKey,
Bucket: testBucket1,
Body: testFileBody
})
.catch(err => expectedError = err)
.then(() => {
Should.exist(expectedError);
expectedError.code.should.eql('NetworkingError');
expectedError.name.should.eql('TimeoutError');
})
});
it('Should throw an error on upload() when readOnly:true', function() {
const testKey = 'myUploadKey';
var expectedError;
return s3ServiceReadonly.upload({
Key: testKey,
Bucket: testBucket1,
Body: testFileBody
})
.catch(err => expectedError = err)
.then(() => {
Should.exist(expectedError);
expectedError.message.should.eql('Readonly service - no CUD operations allowed');
})
});
it('Should throw an error on putObject() when readOnly:true', function() {
const testKey = 'myUploadKey';
var expectedError;
return s3ServiceReadonly.putObject({
Key: testKey,
Bucket: testBucket1,
Body: testFileBody
})
.catch(err => expectedError = err)
.then(() => {
Should.exist(expectedError);
expectedError.message.should.eql('Readonly service - no CUD operations allowed');
})
});
it('Should throw an error on deleteObject() when readOnly:true', function() {
const testKey = 'myUploadKey';
var expectedError;
return s3ServiceReadonly.deleteObject({
Key: testKey,
Bucket: testBucket1
})
.catch(err => expectedError = err)
.then(() => {
Should.exist(expectedError);
expectedError.message.should.eql('Readonly service - no CUD operations allowed');
})
});
it('Should put an object and then get the object with the same key', function () {
const testKey = 'some/key/up/there.txt';
return s3ServiceWithBucket.putObject({
Key: testKey,
Body: testFileBody
})
.then((results) => {
return s3ServiceWithBucket.getObject({
Key: testKey
});
})
.then(result => {
(result.Key).should.eql(testKey);
(result.Body).should.eql(testFileBody);
});
});
it('Should upload a website configuration', () => {
const websiteConfig = {
ErrorDocument: {
Key: 'index.html'
},
IndexDocument: {
Suffix: 'index.html'
}
};
return s3ServiceWithBucket.putBucketWebsite(websiteConfig)
.catch(err => {
(err).should.not.exist();
})
});
});
describe('misc functions', function() {
describe('getBaseEndpointUrl', () => {
it('should return a base url with a path style', () => {
const url = s3ServiceReadonly.getBaseEndpointUrl();
url.should.eql('https://s3.amazonaws.com/vizualai-test/images/tmp')
});
it('should return a base url with a virtual host style', () => {
const baseEndpointUrl = s3ServiceWithBaseUrlBucketAndVirutalHostUrlStyle.getBaseEndpointUrl();
baseEndpointUrl.should.eql('http://vizualai-test.s3.docker/images/tmp');
})
});
describe('stripKeyPrefix', function() {
it('Should strip the key prefix', function () {
const keyToTest = 'tmp/blah/hello/more/key/stuff/file.zip';
const keyWithPrefixStripped = s3ServiceLocalStorageWithBucketAndKeyPrefix.stripKeyPrefix(keyToTest);
keyWithPrefixStripped.should.eql('more/key/stuff/file.zip');
});
it('Should return original key when it does not match the key prefix', function () {
const keyToTest = 'tmp/blah/does/not/match/file.zip';
const keyWithPrefixStripped = s3ServiceLocalStorageWithBucketAndKeyPrefix.stripKeyPrefix(keyToTest);
keyWithPrefixStripped.should.eql(keyToTest);
});
});
describe('getKeyWithKeyPrefix', function() {
it('Should prepend the key prefix if the key does not already have it', function () {
const keyToTest = 'some/key/file.zip';
const keyWithPrefix = s3ServiceLocalStorageWithBucketAndKeyPrefix.getKeyWithKeyPrefix(keyToTest);
keyWithPrefix.should.eql('tmp/blah/hello/some/key/file.zip');
});
it('Should return original key if key already has key prefix', function () {
const keyToTest = 'tmp/blah/hello/more/key/stuff/file.zip';
const keyWithPrefix = s3ServiceLocalStorageWithBucketAndKeyPrefix.getKeyWithKeyPrefix(keyToTest);
keyWithPrefix.should.eql(keyToTest);
});
})
})
});