|
|
@@ -3,6 +3,7 @@ const chai = require('chai')
|
|
|
const { expect } = chai
|
|
|
const SandboxedModule = require('sandboxed-module')
|
|
|
const Errors = require('../../src/Errors')
|
|
|
+const { EventEmitter } = require('events')
|
|
|
|
|
|
const MODULE_PATH = '../../src/S3Persistor.js'
|
|
|
|
|
|
@@ -31,14 +32,15 @@ describe('S3PersistorTests', function () {
|
|
|
|
|
|
let Logger,
|
|
|
Transform,
|
|
|
+ PassThrough,
|
|
|
S3,
|
|
|
Fs,
|
|
|
ReadStream,
|
|
|
Stream,
|
|
|
StreamPromises,
|
|
|
+ S3GetObjectRequest,
|
|
|
S3Persistor,
|
|
|
S3Client,
|
|
|
- S3ReadStream,
|
|
|
S3NotFoundError,
|
|
|
S3AccessDeniedError,
|
|
|
FileNotFoundError,
|
|
|
@@ -55,18 +57,15 @@ describe('S3PersistorTests', function () {
|
|
|
}
|
|
|
|
|
|
Transform = class {
|
|
|
- on(event, callback) {
|
|
|
- if (event === 'readable') {
|
|
|
- callback()
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
once() {}
|
|
|
- removeListener() {}
|
|
|
}
|
|
|
|
|
|
+ PassThrough = class {}
|
|
|
+
|
|
|
Stream = {
|
|
|
Transform,
|
|
|
+ PassThrough,
|
|
|
+ pipeline: sinon.stub().yields(),
|
|
|
}
|
|
|
|
|
|
StreamPromises = {
|
|
|
@@ -77,16 +76,28 @@ describe('S3PersistorTests', function () {
|
|
|
promise: sinon.stub().resolves(),
|
|
|
}
|
|
|
|
|
|
- ReadStream = {
|
|
|
- pipe: sinon.stub().returns('readStream'),
|
|
|
- on: sinon.stub(),
|
|
|
- removeListener: sinon.stub(),
|
|
|
+ ReadStream = new EventEmitter()
|
|
|
+ class FakeS3GetObjectRequest extends EventEmitter {
|
|
|
+ constructor() {
|
|
|
+ super()
|
|
|
+ this.statusCode = 200
|
|
|
+ this.err = null
|
|
|
+ this.aborted = false
|
|
|
+ }
|
|
|
+
|
|
|
+ abort() {
|
|
|
+ this.aborted = true
|
|
|
+ }
|
|
|
+
|
|
|
+ createReadStream() {
|
|
|
+ setTimeout(() => {
|
|
|
+ if (this.err) return ReadStream.emit('error', this.err)
|
|
|
+ this.emit('httpHeaders', this.statusCode)
|
|
|
+ })
|
|
|
+ return ReadStream
|
|
|
+ }
|
|
|
}
|
|
|
- ReadStream.on.withArgs('end').yields()
|
|
|
- ReadStream.on.withArgs('pipe').yields({
|
|
|
- unpipe: sinon.stub(),
|
|
|
- resume: sinon.stub(),
|
|
|
- })
|
|
|
+ S3GetObjectRequest = new FakeS3GetObjectRequest()
|
|
|
|
|
|
FileNotFoundError = new Error('File not found')
|
|
|
FileNotFoundError.code = 'ENOENT'
|
|
|
@@ -101,20 +112,8 @@ describe('S3PersistorTests', function () {
|
|
|
S3AccessDeniedError = new Error('access denied')
|
|
|
S3AccessDeniedError.code = 'AccessDenied'
|
|
|
|
|
|
- S3ReadStream = {
|
|
|
- on: sinon.stub(),
|
|
|
- pipe: sinon.stub(),
|
|
|
- removeListener: sinon.stub(),
|
|
|
- }
|
|
|
- S3ReadStream.on.withArgs('end').yields()
|
|
|
- S3ReadStream.on.withArgs('pipe').yields({
|
|
|
- unpipe: sinon.stub(),
|
|
|
- resume: sinon.stub(),
|
|
|
- })
|
|
|
S3Client = {
|
|
|
- getObject: sinon.stub().returns({
|
|
|
- createReadStream: sinon.stub().returns(S3ReadStream),
|
|
|
- }),
|
|
|
+ getObject: sinon.stub().returns(S3GetObjectRequest),
|
|
|
headObject: sinon.stub().returns({
|
|
|
promise: sinon.stub().resolves({
|
|
|
ContentLength: objectSize,
|
|
|
@@ -171,8 +170,8 @@ describe('S3PersistorTests', function () {
|
|
|
stream = await S3Persistor.getObjectStream(bucket, key)
|
|
|
})
|
|
|
|
|
|
- it('returns a metered stream', function () {
|
|
|
- expect(stream).to.be.instanceOf(Transform)
|
|
|
+ it('returns a PassThrough stream', function () {
|
|
|
+ expect(stream).to.be.instanceOf(PassThrough)
|
|
|
})
|
|
|
|
|
|
it('sets the AWS client up with credentials from settings', function () {
|
|
|
@@ -187,10 +186,16 @@ describe('S3PersistorTests', function () {
|
|
|
})
|
|
|
|
|
|
it('pipes the stream through the meter', async function () {
|
|
|
- expect(S3ReadStream.pipe).to.have.been.calledWith(
|
|
|
- sinon.match.instanceOf(Transform)
|
|
|
+ expect(Stream.pipeline).to.have.been.calledWith(
|
|
|
+ ReadStream,
|
|
|
+ sinon.match.instanceOf(Transform),
|
|
|
+ sinon.match.instanceOf(PassThrough)
|
|
|
)
|
|
|
})
|
|
|
+
|
|
|
+ it('does not abort the request', function () {
|
|
|
+ expect(S3GetObjectRequest.aborted).to.equal(false)
|
|
|
+ })
|
|
|
})
|
|
|
|
|
|
describe('when called with a byte range', function () {
|
|
|
@@ -203,8 +208,8 @@ describe('S3PersistorTests', function () {
|
|
|
})
|
|
|
})
|
|
|
|
|
|
- it('returns a metered stream', function () {
|
|
|
- expect(stream).to.be.instanceOf(Stream.Transform)
|
|
|
+ it('returns a PassThrough stream', function () {
|
|
|
+ expect(stream).to.be.instanceOf(Stream.PassThrough)
|
|
|
})
|
|
|
|
|
|
it('passes the byte range on to S3', function () {
|
|
|
@@ -216,6 +221,23 @@ describe('S3PersistorTests', function () {
|
|
|
})
|
|
|
})
|
|
|
|
|
|
+ describe('when streaming fails', function () {
|
|
|
+ let stream
|
|
|
+
|
|
|
+ beforeEach(async function () {
|
|
|
+ Stream.pipeline.yields(new Error())
|
|
|
+ stream = await S3Persistor.getObjectStream(bucket, key)
|
|
|
+ })
|
|
|
+
|
|
|
+ it('returns a PassThrough stream', function () {
|
|
|
+ expect(stream).to.be.instanceOf(Stream.PassThrough)
|
|
|
+ })
|
|
|
+
|
|
|
+ it('aborts the request', function () {
|
|
|
+ expect(S3GetObjectRequest.aborted).to.equal(true)
|
|
|
+ })
|
|
|
+ })
|
|
|
+
|
|
|
describe('when there are alternative credentials', function () {
|
|
|
let stream
|
|
|
const alternativeSecret = 'giraffe'
|
|
|
@@ -237,8 +259,8 @@ describe('S3PersistorTests', function () {
|
|
|
stream = await S3Persistor.getObjectStream(bucket, key)
|
|
|
})
|
|
|
|
|
|
- it('returns a metered stream', function () {
|
|
|
- expect(stream).to.be.instanceOf(Stream.Transform)
|
|
|
+ it('returns a PassThrough stream', function () {
|
|
|
+ expect(stream).to.be.instanceOf(Stream.PassThrough)
|
|
|
})
|
|
|
|
|
|
it('sets the AWS client up with the alternative credentials', function () {
|
|
|
@@ -289,8 +311,7 @@ describe('S3PersistorTests', function () {
|
|
|
let error, stream
|
|
|
|
|
|
beforeEach(async function () {
|
|
|
- Transform.prototype.on = sinon.stub()
|
|
|
- S3ReadStream.on.withArgs('error').yields(S3NotFoundError)
|
|
|
+ S3GetObjectRequest.statusCode = 404
|
|
|
try {
|
|
|
stream = await S3Persistor.getObjectStream(bucket, key)
|
|
|
} catch (err) {
|
|
|
@@ -319,8 +340,7 @@ describe('S3PersistorTests', function () {
|
|
|
let error, stream
|
|
|
|
|
|
beforeEach(async function () {
|
|
|
- Transform.prototype.on = sinon.stub()
|
|
|
- S3ReadStream.on.withArgs('error').yields(S3AccessDeniedError)
|
|
|
+ S3GetObjectRequest.statusCode = 403
|
|
|
try {
|
|
|
stream = await S3Persistor.getObjectStream(bucket, key)
|
|
|
} catch (err) {
|
|
|
@@ -345,12 +365,11 @@ describe('S3PersistorTests', function () {
|
|
|
})
|
|
|
})
|
|
|
|
|
|
- describe('when S3 encounters an unkown error', function () {
|
|
|
+ describe('when S3 encounters an unknown error', function () {
|
|
|
let error, stream
|
|
|
|
|
|
beforeEach(async function () {
|
|
|
- Transform.prototype.on = sinon.stub()
|
|
|
- S3ReadStream.on.withArgs('error').yields(genericError)
|
|
|
+ S3GetObjectRequest.err = genericError
|
|
|
try {
|
|
|
stream = await S3Persistor.getObjectStream(bucket, key)
|
|
|
} catch (err) {
|
|
|
@@ -480,7 +499,8 @@ describe('S3PersistorTests', function () {
|
|
|
})
|
|
|
|
|
|
it('should meter the stream', function () {
|
|
|
- expect(ReadStream.pipe).to.have.been.calledWith(
|
|
|
+ expect(Stream.pipeline).to.have.been.calledWith(
|
|
|
+ ReadStream,
|
|
|
sinon.match.instanceOf(Stream.Transform)
|
|
|
)
|
|
|
})
|
|
|
@@ -608,12 +628,6 @@ describe('S3PersistorTests', function () {
|
|
|
})
|
|
|
})
|
|
|
|
|
|
- it('should meter the download', function () {
|
|
|
- expect(S3ReadStream.pipe).to.have.been.calledWith(
|
|
|
- sinon.match.instanceOf(Stream.Transform)
|
|
|
- )
|
|
|
- })
|
|
|
-
|
|
|
it('should calculate the md5 hash from the file', function () {
|
|
|
expect(Hash.read).to.have.been.called
|
|
|
})
|