evaporate
Version:
Javascript library for browser to S3 multipart resumable uploads for browsers and with Node FileSystem (fs) Stream Support
697 lines (607 loc) • 21.8 kB
JavaScript
import chai, { expect } from 'chai'
import chaiSinon from 'sinon-chai'
import sinon from 'sinon'
import test from 'ava'
import Evaporate from '../evaporate'
chai.use(chaiSinon)
// consts
const baseConfig = {
signerUrl: 'http://what.ever/sign',
aws_key: 'testkey',
bucket: AWS_BUCKET,
logging: false,
maxRetryBackoffSecs: 0.1,
awsSignatureVersion: '2',
abortCompletionThrottlingMs: 0
}
const baseAddConfig = {
name: AWS_UPLOAD_KEY,
file: new File({
path: '/tmp/file',
size: 50
})
}
let server, fileObject, blobObject, arrayBuffer
function testCommon(t, addConfig, initConfig) {
let evapConfig = Object.assign({}, {awsSignatureVersion: '2'}, initConfig)
return testBase(t, addConfig, evapConfig)
}
function testCancelCallbacks(t) {
const evapConfig = Object.assign({}, baseConfig, {
evaporateChanged: sinon.spy()
})
const config = Object.assign({}, baseAddConfig, {
name: randomAwsKey(),
cancelled: sinon.spy(),
started: function (fileId) { id = fileId; }
})
let id
return testCommon(t, config, evapConfig)
.then(function () {
return t.context.evaporate.cancel(id)
})
}
test.before(() => {
sinon.xhr.supportsCORS = true
global.XMLHttpRequest = sinon.useFakeXMLHttpRequest()
server = serverCommonCase()
fileObject = global.File
blobObject = global.Blob
arrayBuffer = global.FileReader.prototype.readAsArrayBuffer
})
test.beforeEach((t) =>{
beforeEachSetup(t, new File({
path: '/tmp/file',
size: 50,
name: randomAwsKey()
}))
})
test('should work', (t) => {
expect(true).to.be.ok
})
// constructor
test('#create should return supported instance', (t) => {
return Evaporate.create(baseConfig)
.then(function (evaporate) {
expect(evaporate).to.be.instanceof(Evaporate)
},
function (reason) {
t.fail(reason)
})
})
test('#create evaporate should support #add', (t) => {
return Evaporate.create(baseConfig)
.then(function (evaporate) {
expect(evaporate.add).to.be.instanceof(Function)
},
function (reason) {
t.fail(reason)
})
})
test('#create evaporate should support #cancel', (t) => {
return Evaporate.create(baseConfig)
.then(function (evaporate) {
expect(evaporate.cancel).to.be.instanceof(Function)
},
function (reason) {
t.fail(reason)
})
})
test('#create evaporate should support #pause', (t) => {
return Evaporate.create(baseConfig)
.then(function (evaporate) {
expect(evaporate.pause).to.be.instanceof(Function)
},
function (reason) {
t.fail(reason)
})
})
test('#create evaporate should support #resume', (t) => {
return Evaporate.create(baseConfig)
.then(function (evaporate) {
expect(evaporate.resume).to.be.instanceof(Function)
},
function (reason) {
t.fail(reason)
})
})
test('#create evaporate should support #forceRetry', (t) => {
return Evaporate.create(baseConfig)
.then(function (evaporate) {
expect(evaporate.forceRetry).to.be.instanceof(Function)
},
function (reason) {
t.fail(reason)
})
})
// local time offset
test('#create evaporate should use default local time offset without a timeUrl', (t) => {
return Evaporate.create(baseConfig)
.then(function (evaporate) {
expect(evaporate.localTimeOffset).to.equal(0)
},
function (reason) {
t.fail(reason)
})
})
test('#create evaporate should respect localTimeOffset', (t) => {
var offset = 30,
config = Object.assign({}, baseConfig, { localTimeOffset: offset })
return Evaporate.create(config)
.then(function (evaporate) {
expect(evaporate.localTimeOffset).to.equal(30)
},
function (reason) {
t.fail(reason)
})
})
test('#create evaporate should respect returned server time from timeUrl when local is behind server', (t) => {
var config = Object.assign({}, baseConfig, { timeUrl: 'http://example.com/time?testId=' + t.context.testId })
t.context.timeUrlDate = new Date(new Date().setTime(new Date().getTime() + (60 * 60 * 1000)))
return Evaporate.create(config)
.then(function (evaporate) {
expect(evaporate.localTimeOffset).to.be.closeTo(+3600000, 100)
},
function (reason) {
t.fail(reason)
})
})
test('#create evaporate should respect returned server time from timeUrl when server is behind local', (t) => {
var config = Object.assign({}, baseConfig, { timeUrl: 'http://example.com/time?testId=' + t.context.testId })
t.context.timeUrlDate = new Date(new Date().setTime(new Date().getTime() - (60 * 60 * 1000)))
return Evaporate.create(config)
.then(function (evaporate) {
expect(evaporate.localTimeOffset).to.be.closeTo(-3600000, 100)
},
function (reason) {
t.fail(reason)
})
})
test('#create evaporate calls timeUrl only once', (t) => {
var config = Object.assign({}, baseConfig, { timeUrl: 'http://example.com/time?testId=' + t.context.testId })
return Evaporate.create(config)
.then(function () {
expect(t.context.timeUrlCalled).to.equal(1)
},
function (reason) {
t.fail(reason)
})
})
test('new Evaporate() should instantiate and return default offset before timeUrl', (t) => {
var config = Object.assign({}, baseConfig, { timeUrl: 'http://example.com/time' })
expect(new Evaporate(config).localTimeOffset).to.equal(0)
})
test('new Evaporate() should instantiate and not call timeUrl', (t) => {
var config = Object.assign({}, baseConfig, { timeUrl: 'http://example.com/time' })
var evaporate = newEvaporate(t, config);
return evaporateAdd(t, evaporate, config)
.then(function () {
expect(typeof t.context.timeUrlCalled).to.equal('undefined')
})
})
test('new Evaporate() calls timeUrl only once', (t) => {
var config = Object.assign({}, baseConfig, { timeUrl: 'http://example.com/time?testId=' + t.context.testId })
expect(new Evaporate(config).localTimeOffset).to.equal(0)
})
// Unsupported
test('should require configuration options on instantiation', (t) => {
return Evaporate.create()
.then(function () {
t.fail('Evaporate instantiated but should not have.')
},
function (reason) {
t.pass(reason)
})
})
test('should signerUrl is required unless signResponseHandler is present', (t) => {
return Evaporate.create({signerUrl: null, signResponseHandler: null})
.then(function () {
t.fail('Evaporate instantiated but should not have.')
},
function (reason) {
t.pass(reason)
})
})
test('should require an AWS bucket with a signerUrl', (t) => {
return Evaporate.create({signerUrl: 'https://sign.com/sign'})
.then(function () {
t.fail('Evaporate instantiated but should not have.')
},
function (reason) {
t.pass(reason)
})
})
test('should require an AWS bucket without a signerUrl but with a signResponseHandler', (t) => {
return Evaporate.create({signResponseHandler: function () {}})
.then(function () {
t.fail('Evaporate instantiated but should not have.')
},
function (reason) {
t.pass(reason)
})
})
test('should require a cryptoMd5Method if computeContentMd5 is enabled', (t) => {
return Evaporate.create({bucket: 'asdafsa', signerUrl: 'https://sign.com/sign', computeContentMd5: true})
.then(function () {
t.fail('Evaporate instantiated but should not have.')
},
function (reason) {
t.pass(reason)
})
})
test('should require a cryptoHexEncodedHash256 method if computeContentMd5 is enabled with V4 signatures', (t) => {
return Evaporate.create({
bucket: 'asdafsa',
signerUrl: 'https://sign.com/sign',
computeContentMd5: true,
awsSignatureVersion: '4',
cryptoMd5Method: function () {}
})
.then(function () {
t.fail('Evaporate instantiated but should not have.')
},
function (reason) {
t.pass(reason)
})
})
test('should require computeContentMd5 if V4 signatures enabled', (t) => {
return Evaporate.create({bucket: 'asdafsa', signerUrl: 'https://sign.com/sign', awsSignatureVersion: '4'})
.then(function () {
t.fail('Evaporate instantiated but should not have.')
},
function (reason) {
t.pass(reason)
})
})
test.serial('should require browser File support', (t) => {
global['File'] = undefined
return Evaporate.create({bucket: 'asdafsa', signerUrl: 'https://sign.com/sign', awsSignatureVersion: '2'})
.then(function () {
global.File = fileObject
t.fail('Evaporate instantiated but should not have.')
},
function (reason) {
global.File = fileObject
t.pass(reason)
})
})
test.serial('should require browser Blob support', (t) => {
global['Blob'] = undefined
return Evaporate.create({bucket: 'asdafsa', signerUrl: 'https://sign.com/sign', awsSignatureVersion: '2'})
.then(function () {
global.Blob = blobObject
t.fail('Evaporate instantiated but should not have.')
},
function (reason) {
global.Blob = blobObject
t.pass(reason)
})
})
test.serial('should require browser FileReader#readAsArrayBuffer support if computeContentMd5 enabled', (t) => {
global['FileReader'].prototype.readAsArrayBuffer = undefined
return Evaporate.create({bucket: 'asdafsa', signerUrl: 'https://sign.com/sign', awsSignatureVersion: '2', computeContentMd5: true, cryptoMd5Method: function () {}})
.then(function () {
global['FileReader'].prototype.readAsArrayBuffer = arrayBuffer
t.fail('Evaporate instantiated but should not have.')
},
function (reason) {
global['FileReader'].prototype.readAsArrayBuffer = arrayBuffer
t.pass(reason)
})
})
test.todo('should require browser Blob slice support')
test.todo('should require browser Promise support')
test.todo('should validate readableStream and readableStreamPartMethod')
// add
test('should fail to add() when no file is present', (t) => {
return Evaporate.create(baseConfig)
.then(function (evaporate) {
evaporate.add({ name: 'test' })
.then(function () {
t.fail('Evaporate added a new file but should not have.')
},
function (reason) {
expect(reason).to.match(/missing file/i)
})
})
});
test('should fail to add() when empty config is present', (t) => {
return Evaporate.create(baseConfig)
.then(function (evaporate) {
evaporate.add({})
.then(function () {
t.fail('Evaporate added a new file but should not have.')
},
function (reason) {
t.pass(reason)
})
})
});
test('should fail to add() when no config is present', (t) => {
return Evaporate.create(baseConfig)
.then(function (evaporate) {
evaporate.add()
.then(function () {
t.fail('Evaporate added a new file but should not have.')
},
function (reason) {
t.pass(reason)
})
})
});
test('should require a name if file is present', (t) => {
return Evaporate.create(baseConfig)
.then(function (evaporate) {
evaporate.add({
file: new File({
path: '/tmp/file',
size: 50000
})
})
.then(function () {
t.fail('Evaporate added a new file but should not have.')
},
function (reason) {
t.pass(reason)
})
})
});
test('should respect maxFileSize', (t) => {
return Evaporate.create(Object.assign({}, baseConfig, {maxFileSize: 10}))
.then(function (evaporate) {
evaporate.add({
file: new File({
path: '/tmp/file',
size: 50000
})
})
.then(function () {
t.fail('Evaporate added a new file but should not have.')
},
function (reason) {
t.pass(reason)
})
})
});
test('should add() new upload with correct config', (t) => {
return testCommon(t)
.then(function (fileKey) {
let id = fileKey;
expect(id).to.equal(t.context.requestedAwsObjectKey)
})
})
test('should add() new upload with correct completed XML', (t) => {
return testCommon(t)
.then(function () {
expect(testRequests[t.context.testId][5].requestBody).to.equal('<CompleteMultipartUpload><Part><PartNumber>1</PartNumber><ETag></ETag></Part></CompleteMultipartUpload>')
})
})
test('should return fileKeys correctly for common cases started', (t) => {
let config = Object.assign({}, baseAddConfig, {
started: function (fileKey) { start_id = fileKey; }
})
let start_id
return testCommon(t, config)
.then(function () {
let expected = baseConfig.bucket + '/' + config.name
expect(start_id).to.equal(expected)
})
})
test('should return fileKeys correctly for common cases resolve', (t) => {
return testCommon(t)
.then(function (fileKey) {
let expected = t.context.config.name
expect(fileKey).to.equal(expected)
})
})
test('should return the object key in the complete callback', (t) => {
let complete_id
let config = Object.assign({}, baseAddConfig, {
complete: sinon.spy(function (xhr, name) { complete_id = name; })
})
return testCommon(t, config)
.then(function () {
expect(complete_id).to.equal(config.name)
expect(t.context.config.complete.firstCall.args.length).to.equal(3)
expect(t.context.config.complete.firstCall.args[0]).to.be.instanceOf(sinon.FakeXMLHttpRequest)
expect(typeof t.context.config.complete.firstCall.args[1]).to.equal('string')
expect(typeof t.context.config.complete.firstCall.args[2]).to.equal('object')
})
})
test('should correctly encode parentheses for S3', (t) => {
let complete_id
const c = Object.assign({}, baseAddConfig, {name: '()name()'})
let config = Object.assign({}, c, {
complete: sinon.spy(function (xhr, name) { complete_id = name; })
})
return testCommon(t, config)
.then(function () {
expect(complete_id).to.equal('%28%29name%28%29')
})
})
test('should correctly encode single quotes, exclamation points for S3', (t) => {
let complete_id
const c = Object.assign({}, baseAddConfig, {name: "'na!me'"})
let config = Object.assign({}, c, {
complete: sinon.spy(function (xhr, name) { complete_id = name; })
})
return testCommon(t, config)
.then(function () {
expect(complete_id).to.equal('%27na%21me%27')
})
})
test('should correctly encode asterisk for S3', (t) => {
let complete_id
const c = Object.assign({}, baseAddConfig, {name: "foo*"})
let config = Object.assign({}, c, {
complete: sinon.spy(function (xhr, name) { complete_id = name; })
})
return testCommon(t, config)
.then(function () {
expect(complete_id).to.equal('foo%2A')
})
})
test('should correctly encode spaces for S3', (t) => {
let complete_id
const c = Object.assign({}, baseAddConfig, {name: " name "})
let config = Object.assign({}, c, {
complete: sinon.spy(function (xhr, name) { complete_id = name; })
})
return testCommon(t, config)
.then(function () {
expect(complete_id).to.equal('%20name%20')
})
})
test('should add() two new uploads with correct config', (t) => {
let id0, id1
let config1 = Object.assign({}, baseAddConfig, {
started: function (fileId) { id0 = fileId; }
})
let config2 = Object.assign({}, config1, {
name: randomAwsKey(),
started: function (fileId) { id1 = fileId;},
})
let promise1 = testCommon(t, config1)
let promise2 = testCommon(t, config2)
return Promise.all([promise1, promise2])
.then (function () {
expect(id0).to.equal(baseConfig.bucket + '/' + config1.name);
expect(id1).to.equal(baseConfig.bucket + '/' + config2.name);
})
})
test('should call a callback on successful add()', (t) => {
return testCommon(t)
.then(function () {
expect(t.context.config.started.withArgs(baseConfig.bucket + '/' + t.context.requestedAwsObjectKey).calledOnce).to.be.true
})
})
test('should call a callback on successful initiate()', (t) => {
const initated_spy = sinon.spy()
const config = Object.assign({}, baseAddConfig, {
uploadInitiated: initated_spy
})
return testCommon(t, config)
.then(function () {
expect(initated_spy.withArgs('Hzr2sK034dOrV4gMsYK.MMrtWIS8JVBPKgeQ.LWd6H8V2PsLecsBqoA1cG1hjD3G4KRX_EBEwxWWDu8lNKezeA--').calledOnce).to.be.true
})
})
test('should call a progress with stats callback on successful add()', (t) => {
return testCommon(t, {progress: sinon.spy()})
.then(function () {
expect(t.context.config.progress.firstCall.args.length).to.equal(2)
expect(typeof t.context.config.progress.firstCall.args[1]).to.equal('object')
})
})
// cancel
test('should fail with a message when canceling all if no files are processing', (t) => {
return Evaporate.create(baseConfig)
.then(function (evaporate) {
return evaporate.cancel()
.then(function () {
t.fail('Cancel did not fail.')
})
.catch(function (reason) {
expect(reason).to.match(/no files to cancel/i)
})
})
})
test('should cancel() all uploads when cancel receives no parameters', (t) => {
const config = Object.assign({}, baseAddConfig, {
name: randomAwsKey(),
started: function (fileId) { id = fileId; }
})
let id
return testCommon(t, config)
.then(function () {
t.context.evaporate.cancel()
.then(function () {
t.fail('Expected test to fail.')
})
.catch(function (reason) {
expect(reason).to.match(/no files to cancel/i)
})
})
})
test('should fail to cancel() when non-existing id is present', (t) => {
return Evaporate.create(baseConfig)
.then(function (evaporate) {
evaporate.cancel('non-existent-file')
.then(function () {
t.fail('Cancel did not fail.')
})
.catch(function (reason) {
expect(reason).to.match(/does not exist/i)
})
})
})
test('should cancel() an upload with correct object name', (t) => {
const config = Object.assign({}, baseAddConfig, {
name: randomAwsKey(),
started: function (fileId) { id = fileId; }
})
let id
return testCommon(t, config)
.then(function () {
const result = t.context.evaporate.cancel(id)
expect(result).to.be.ok
})
})
test('should cancel() two uploads with correct id, first result OK', (t) => {
let config1 = Object.assign({}, baseAddConfig, {
started: function (fileId) { id0 = fileId;}
})
let config2 = Object.assign({}, config1, {
name: randomAwsKey(),
started: function (fileId) { id1 = fileId;},
})
let id0, id1
let promise0 = testCommon(t, config1)
let promise1 = testCommon(t, config2)
return Promise.all([promise0, promise1])
.catch(function (reason) {
t.fail('Promises failed.')
})
})
test('should call a callbacks on cancel(): canceled', (t) => {
return testCancelCallbacks(t)
.then(function () {
expect(t.context.config.cancelled).to.have.been.called
})
})
test('should call a callbacks on cancel(): evaporateChanged', (t) => {
return testCancelCallbacks(t)
.then(function () {
expect(t.context.evapConfig.evaporateChanged).to.have.been.called
})
})
test('should call a callbacks on cancel(): evaporateChanged call count', (t) => {
return testCancelCallbacks(t)
.then(function () {
expect(t.context.evapConfig.evaporateChanged.callCount).to.equal(2)
})
})
test('should call a callbacks on cancel(): evaporateChanged first call args', (t) => {
return testCancelCallbacks(t)
.then(function () {
expect(t.context.evapConfig.evaporateChanged.firstCall.args[1]).to.eql(1)
})
})
test('should call a callbacks on cancel(): evaporateChanged second call args', (t) => {
return testCancelCallbacks(t)
.then(function () {
expect(t.context.evapConfig.evaporateChanged.secondCall.args[1]).to.eql(0)
})
})
// configuration overrides
test('should add() new upload with correct config with custom bucket on add', (t) => {
let evapConfig = Object.assign({}, {awsSignatureVersion: '2'}, baseConfig)
const evaporate = newEvaporate(t, evapConfig)
let a1 = evaporateAdd(t, evaporate, baseAddConfig, { bucket: 'fileCustomBucket1' })
let a2 = evaporateAdd(t, evaporate, baseAddConfig, { bucket: 'fileCustomBucket2' })
return Promise.all([a1, a2])
.then(function () {
expect(testRequests[t.context.testId][1].url).to.match(new RegExp('fileCustomBucket1'))
var last = testRequests[t.context.testId].length - 1
expect(testRequests[t.context.testId][last].url).to.match(new RegExp('fileCustomBucket2'))
})
})