UNPKG

evaporate

Version:

Javascript library for browser to S3 multipart resumable uploads for browsers and with Node FileSystem (fs) Stream Support

408 lines (375 loc) 13.2 kB
import { expect } from 'chai' import sinon from 'sinon' import test from 'ava' // constants let server function testCommon(t, addConfig, initConfig) { let evapConfig = Object.assign({}, {awsSignatureVersion: '2'}, initConfig) return testBase(t, addConfig, evapConfig) } function testMd5V2(t) { return testCommon(t, {}, { awsSignatureVersion: '2', computeContentMd5: true }) } function testMd5V4(t) { return testCommon(t, {}, { computeContentMd5: true, cryptoHexEncodedHash256: function (d) { return d; } }) } function testSignerErrors(t, errorStatus, evapConfig) { t.context.retry = function (type) { return type === 'sign' } t.context.errorStatus = errorStatus function requestOrder() { let request_order = [], requestMap = { 'GET:to_sign': 'sign', 'POST:uploads': 'initiate', 'POST:uploadId': 'complete', 'DELETE:uploadId': 'cancel', 'GET:uploadId': 'check for parts' }, requests = testRequests[t.context.testId] || [] requests.forEach(function (r) { var x = r.url.split('?'), y = x[1] ? x[1].split('&') : '', z = y[0] ? y[0].split('=')[0] : y if (z === 'partNumber') { z += '=' z += y[0].split('=')[1] } var v = z ? r.method + ':' + z : r.method request_order.push(requestMap[v] || v) }) return request_order.join(',') } return testCommon(t, { file: new File({ path: '/tmp/file', size: 50, name: 'tests' })}, evapConfig) .then(function () { return Promise.resolve(requestOrder(t)); }) .catch(function (reason) { return Promise.reject(reason + " " + requestOrder(t)); }) } test.before(() => { sinon.xhr.supportsCORS = true global.XMLHttpRequest = sinon.useFakeXMLHttpRequest() global.window = { localStorage: {}, console: console }; server = serverCommonCase() }) test.beforeEach((t) => { beforeEachSetup(t) }) // Default Setup: V2 signatures: Common Case test('should not call cryptoMd5 upload a file with defaults and V2 signature', (t) => { return testCommon(t, {}, { awsSignatureVersion: '2' }) .then(function () { expect(t.context.cryptoMd5.callCount).to.equal(0) }) }) test('should upload a file with S3 requests in the correct order', (t) => { return testCommon(t) .then(function () { expect(requestOrder(t)).to.equal('initiate,PUT:partNumber=1,PUT:partNumber=2,complete') }) }) test('should upload a file and return the correct file upload ID', (t) => { return testCommon(t) .then(function () { expect(t.context.completedAwsKey).to.equal(t.context.requestedAwsObjectKey) }) }) test('should upload a file and callback complete once', (t) => { return testCommon(t) .then(function () { expect(t.context.config.complete.calledOnce).to.be.true }) }) test('should upload a file and callback complete with first param instance of xhr', (t) => { return testCommon(t) .then(function () { expect(t.context.config.complete.firstCall.args[0]).to.be.instanceOf(sinon.FakeXMLHttpRequest) }) }) test('should upload a file and callback complete with second param the awsKey', (t) => { return testCommon(t) .then(function () { expect(t.context.config.complete.firstCall.args[1]).to.equal(t.context.requestedAwsObjectKey) }) }) test('should upload a file and not callback with a changed object name', (t) => { return testCommon(t, {nameChanged: sinon.spy()}) .then(function () { expect(t.context.config.nameChanged.callCount).to.equal(0) }) }) // md5Digest tests test('V2 should call cryptoMd5 when uploading a file with defaults', (t) => { return testMd5V2(t) .then(function () { expect(t.context.cryptoMd5.callCount).to.equal(2) }) }) test('V2 should upload a file with MD5Digests with S3 requests in the correct order', (t) => { return testMd5V2(t) .then(function () { expect(requestOrder(t)).to.equal('initiate,PUT:partNumber=1,PUT:partNumber=2,complete') }) }) test('V2 should upload a file and return the correct file upload ID', (t) => { return testMd5V2(t) .then(function () { expect(t.context.completedAwsKey).to.equal(t.context.requestedAwsObjectKey) }) }) test('V4 should call cryptoMd5 when uploading a file with defaults', (t) => { return testMd5V4(t) .then(function () { expect(t.context.cryptoMd5.callCount).to.equal(2) }) }) test('V4 should upload a file with MD5Digests with S3 requests in the correct order', (t) => { return testMd5V4(t) .then(function () { expect(requestOrder(t)).to.equal('initiate,PUT:partNumber=1,PUT:partNumber=2,complete') }) }) test('V4 should upload a file and return the correct file upload ID', (t) => { return testMd5V4(t) .then(function () { expect(t.context.completedAwsKey).to.equal(t.context.requestedAwsObjectKey) }) }) // Cover xAmzHeader Options test('should pass xAmzHeadersAtInitiate headers', (t) => { return testCommon(t, { xAmzHeadersAtInitiate: { 'x-custom-header': 'peanuts' } }) .then(function () { expect(headersForMethod(t, 'POST', /^.*\?uploads.*$/)['x-custom-header']).to.equal('peanuts') }) }) test('should pass xAmzHeadersAtUpload headers', (t) => { return testCommon(t, { xAmzHeadersAtUpload: { 'x-custom-header': 'phooey' } }) .then(function () { expect(headersForMethod(t, 'PUT')['x-custom-header']).to.equal('phooey') }) }) test('should pass xAmzHeadersAtComplete headers', (t) => { return testCommon(t, { xAmzHeadersAtComplete: { 'x-custom-header': 'eindelijk' } }) .then(function () { expect(headersForMethod(t, 'POST', /.*\?uploadId.*$/)['x-custom-header']).to.equal('eindelijk') }) }) test('should not use xAmzHeadersCommon headers for Initiate', (t) => { return testCommon(t, { xAmzHeadersAtInitiate: { 'x-custom-header': 'peanuts' }, xAmzHeadersCommon: { 'x-custom-header': 'phooey' } }) .then(function () { expect(headersForMethod(t, 'POST', /^.*\?uploads.*$/)['x-custom-header']).to.equal('peanuts') }) }) test('should use xAmzHeadersCommon headers for Parts', (t) => { return testCommon(t, { xAmzHeadersCommon: { 'x-custom-header': 'phooey' } }) .then(function () { expect(headersForMethod(t, 'PUT')['x-custom-header']).to.equal('phooey') }) }) test('should use xAmzHeadersCommon headers for Complete', (t) => { return testCommon(t, { xAmzHeadersCommon: { 'x-custom-header': 'phooey' } }) .then(function () { expect(headersForMethod(t, 'POST', /.*\?uploadId.*$/)['x-custom-header']).to.equal('phooey') }) }) test('should let xAmzHeadersCommon override xAmzHeadersAtUpload (1)', (t) => { return testCommon(t, { xAmzHeadersAtUpload: { 'x-custom-header1': 'phooey' }, xAmzHeadersCommon: { 'x-custom-header3': 'phooey' } }) .then(function () { expect(headersForMethod(t, 'PUT')['x-custom-header3']).to.equal('phooey') }) }) test('should let xAmzHeadersCommon override xAmzHeadersAtUpload (2)', (t) => { return testCommon(t, { xAmzHeadersAtUpload: { 'x-custom-header1': 'phooey' }, xAmzHeadersCommon: { 'x-custom-header3': 'phooey' } }) .then(function () { expect(headersForMethod(t, 'PUT')['x-custom-header1']).to.equal(undefined) }) }) test('should let xAmzHeadersCommon override xAmzHeadersAtComplete (1)', (t) => { return testCommon(t, { xAmzHeadersAtComplete: { 'x-custom-header2': 'phooey' }, xAmzHeadersCommon: { 'x-custom-header3': 'phooey' } }) .then(function () { expect(headersForMethod(t, 'POST', /.*\?uploadId.*$/)['x-custom-header3']).to.equal('phooey') }) }) test('should let xAmzHeadersCommon override xAmzHeadersAtComplete (2)', (t) => { return testCommon(t, { xAmzHeadersAtComplete: { 'x-custom-header2': 'phooey' }, xAmzHeadersCommon: { 'x-custom-header3': 'phooey' } }) .then(function () { expect(headersForMethod(t, 'POST', /.*\?uploadId.*$/)['x-custom-header2']).to.equal(undefined) }) }) // Retry on Errors test('should retry Initiate', (t) => { t.context.retry = function (type) { return type === 'init' } return testCommon(t, {}) .then(function () { expect(['initiate,initiate,PUT:partNumber=1,PUT:partNumber=2,complete', 'initiate,initiate,PUT:partNumber=2,PUT:partNumber=1,complete']).to.include('initiate,initiate,PUT:partNumber=1,PUT:partNumber=2,complete') }) }) test('should retry Complete', (t) => { t.context.retry = function (type) { return type === 'complete' } return testCommon(t, {}) .then(function () { expect(requestOrder(t)).to.equal('initiate,PUT:partNumber=1,PUT:partNumber=2,complete,complete') }) }) test('should retry Upload Part', (t) => { t.context.retry = function (type) { return type === 'part' } return testCommon(t, { file: new File({ path: '/tmp/file', size: 50, name: 'tests' }) }) .then(function () { expect(requestOrder(t)).to.equal('initiate,PUT:partNumber=1,PUT:partNumber=1,complete') }) }) // Retry get authorization / Initiate Upload test('should retry get signature for common case: Initiate, Put, Complete (authorization), for non-permission responses', (t) => { return testSignerErrors(t, 500) .then(function (result) { expect(result).to.equal('sign,sign,initiate,sign,sign,PUT:partNumber=1,sign,sign,complete') }) }) test('should not retry get signature for common case: Initiate, Put, Complete (authorization), for permission 401', (t) => { return testSignerErrors(t, 401) .then(function (result) { t.fail('Expected test to fail but received: ' + result) }) .catch(function (reason) { expect(reason).to.equal('Permission denied status:401 sign') }) }) test('should not retry get signature for common case: Initiate, Put, Complete (authorization), for permission 403', (t) => { return testSignerErrors(t, 403) .then(function (result) { t.fail('Expected test to fail but received: ' + result) }) .catch(function (reason) { expect(reason).to.equal('Permission denied status:403 sign') }) }) test('should not retry customAuthMethod for common case: Initiate, Put, Complete (authorization) if it rejects', (t) => { const customRejectingAuthHandler = function () { return Promise.reject('Permission denied'); } return testSignerErrors(t, 403, {signerUrl: undefined, customAuthMethod: customRejectingAuthHandler}) .then(function (result) { t.fail('Expected test to fail but received: ' + result) }) .catch(function (reason) { expect(reason).to.equal('Permission denied ') }) }) // Failures to upload because PUT Part 404 test('should fail if PUT part 404s', (t) => { t.context.retry = function (type) { return type === 'part' } t.context.errorStatus = 404 return testCommon(t) .then(function () { t.fail('Expected upload to fail but it did not.') }) .catch(function (reason) { expect(reason).to.match(/File upload aborted/i) }) }) test('should call cancelled() if PUT part 404s', (t) => { t.context.retry = function (type) { return type === 'part' } t.context.errorStatus = 404 return testCommon(t, { cancelled: sinon.spy() }) .then(function () { t.fail('Expected upload to fail but it did not.') }) .catch(function () { expect(t.context.config.cancelled.callCount).to.equal(1) }) }) test('should call the correctly ordered requests if PUT part 404s', (t) => { t.context.retry = function (type) { return type === 'part' } t.context.errorStatus = 404 return testCommon(t) .then(function () { t.fail('Expected upload to fail but it did not.') }) .catch(function () { expect(requestOrder(t)).to.equal('initiate,PUT:partNumber=1,cancel') }) }) test('should fail with a message when PUT part 404s and DELETE fails', (t) => { t.context.retry = function (type) { return type === 'part' } t.context.errorStatus = 404 t.context.deleteStatus = 403 return testCommon(t, { cancelled: sinon.spy() }) .then(function () { t.fail('Expected upload to fail but it did not.') }) .catch(function (reason) { expect(reason).to.match(/Error aborting upload/i) }) }) test('should fail with the correctly ordered requests when PUT part 404s and DELETE fails', (t) => { t.context.retry = function (type) { return type === 'part' } t.context.errorStatus = 404 t.context.deleteStatus = 403 return testCommon(t, { cancelled: sinon.spy() }) .then(function () { t.fail('Expected upload to fail but it did not.') }) .catch(function () { expect(requestOrder(t)).to.match(/initiate,PUT:partNumber=1,cancel,cancel/) }) })