mss-sdk
Version:
MSS SDK for JavaScript
504 lines (416 loc) • 18.2 kB
text/coffeescript
helpers = require('./helpers')
AWS = helpers.AWS
MockService = helpers.MockService
describe 'AWS.EventListeners', ->
oldSetTimeout = setTimeout
config = null; service = null; totalWaited = null; delays = []
successHandler = null; errorHandler = null; completeHandler = null
retryHandler = null
beforeEach ->
`setTimeout = helpers.createSpy('setTimeout');`
setTimeout.andCallFake (callback, delay) ->
totalWaited += delay
delays.push(delay)
callback()
totalWaited = 0
delays = []
service = new MockService(maxRetries: 3)
service.config.credentials = AWS.util.copy(service.config.credentials)
successHandler = helpers.createSpy('success')
errorHandler = helpers.createSpy('error')
completeHandler = helpers.createSpy('complete')
retryHandler = helpers.createSpy('retry')
afterEach -> `setTimeout = oldSetTimeout`
makeRequest = (callback) ->
request = service.makeRequest('mockMethod', foo: 'bar')
request.on('retry', retryHandler)
request.on('error', errorHandler)
request.on('success', successHandler)
request.on('complete', completeHandler)
if callback
request.send(callback)
else
request
describe 'validate', ->
it 'takes the request object as a parameter', ->
request = makeRequest()
request.on 'validate', (req) ->
expect(req).to.equal(request)
throw "ERROR"
response = request.send(->)
expect(response.error).to.equal("ERROR")
it 'sends error event if credentials are not set', ->
service.config.credentialProvider = null
service.config.credentials.accessKeyId = null
makeRequest(->)
expect(errorHandler.calls.length).not.to.equal(0)
AWS.util.arrayEach errorHandler.calls, (call) ->
expect(call.arguments[0]).to.be.instanceOf(Error)
expect(call.arguments[0].code).to.equal('CredentialsError')
expect(call.arguments[0].message).to.match(/Missing credentials/)
it 'sends error event if credentials are not set', ->
service.config.credentials.accessKeyId = 'akid'
service.config.credentials.secretAccessKey = null
makeRequest(->)
expect(errorHandler.calls.length).not.to.equal(0)
AWS.util.arrayEach errorHandler.calls, (call) ->
expect(call.arguments[0] ).to.be.instanceOf(Error)
expect(call.arguments[0].code).to.equal('CredentialsError')
expect(call.arguments[0].message).to.match(/Missing credentials/)
it 'does not validate credentials if request is not signed', ->
helpers.mockHttpResponse 200, {}, ''
service.api = new AWS.Model.Api metadata:
endpointPrefix: 'mockservice'
signatureVersion: null
request = makeRequest()
request.send(->)
expect(errorHandler.calls.length).to.equal(0)
expect(successHandler.calls.length).not.to.equal(0)
it 'sends error event if region is not set', ->
helpers.mockHttpResponse 200, {}, ''
service.config.region = null
request = makeRequest(->)
call = errorHandler.calls[0]
expect(errorHandler.calls.length).not.to.equal(0)
expect(call.arguments[0] ).to.be.instanceOf(Error)
expect(call.arguments[0].code).to.equal('ConfigError')
expect(call.arguments[0].message).to.match(/Missing region in config/)
it 'ignores region validation if service has global endpoint', ->
helpers.mockHttpResponse 200, {}, ''
service.config.region = null
service.isGlobalEndpoint = true
makeRequest(->)
expect(errorHandler.calls.length).to.equal(0)
delete service.isGlobalEndpoint
describe 'build', ->
it 'takes the request object as a parameter', ->
helpers.mockHttpResponse 200, {}, ''
request = makeRequest()
request.on 'build', (req) ->
expect(req).to.equal(request)
throw "ERROR"
response = request.send(->)
expect(response.error).to.equal("ERROR")
describe 'afterBuild', ->
sendRequest = (body) ->
request = makeRequest()
request.removeAllListeners('sign')
request.on('build', (req) -> req.httpRequest.body = body)
request.build()
request
contentLength = (body) ->
sendRequest(body).httpRequest.headers['Content-Length']
it 'builds Content-Length in the request headers for string content', ->
expect(contentLength('FOOBAR')).to.equal(6)
it 'builds Content-Length for string "0"', ->
expect(contentLength('0')).to.equal(1)
it 'builds Content-Length for utf-8 string body', ->
expect(contentLength('tï№')).to.equal(6)
it 'builds Content-Length for buffer body', ->
expect(contentLength(new AWS.util.Buffer('tï№'))).to.equal(6)
if AWS.util.isNode()
it 'builds Content-Length for file body', ->
fs = require('fs')
file = fs.createReadStream(__filename)
fileLen = fs.lstatSync(file.path).size
expect(contentLength(file)).to.equal(fileLen)
describe 'sign', ->
it 'takes the request object as a parameter', ->
helpers.mockHttpResponse 200, {}, ''
request = makeRequest()
request.on 'sign', (req) ->
expect(req).to.equal(request)
throw "ERROR"
response = request.send(->)
expect(response.error).to.equal("ERROR")
it 'uses the api.signingName if provided', ->
helpers.mockHttpResponse 200, {}, ''
service.api.signingName = 'SIGNING_NAME'
helpers.spyOn(AWS.Signers.RequestSigner, 'getVersion').andCallFake ->
(req, signingName) -> throw signingName
request = makeRequest()
response = request.send(->)
expect(response.error).to.equal('SIGNING_NAME')
delete service.api.signingName
it 'uses the api.endpointPrefix if signingName not provided', ->
helpers.mockHttpResponse 200, {}, ''
helpers.spyOn(AWS.Signers.RequestSigner, 'getVersion').andCallFake ->
(req, signingName) -> throw signingName
request = makeRequest()
response = request.send(->)
expect(response.error).to.equal('mockservice')
describe 'send', ->
it 'passes httpOptions from config', ->
options = {}
helpers.spyOn(AWS.HttpClient, 'getInstance').andReturn handleRequest: (req, opts) ->
options = opts
new AWS.SequentialExecutor()
service.config.httpOptions = timeout: 15
service.config.maxRetries = 0
makeRequest(->)
expect(options.timeout).to.equal(15)
it 'signs only once in normal case', ->
signHandler = helpers.createSpy('sign')
helpers.mockHttpResponse 200, {}, ['data']
request = makeRequest()
request.on('sign', signHandler)
request.build()
request.signedAt = new Date(request.signedAt - 60 * 5 * 1000)
request.send()
expect(signHandler.calls.length).to.equal(1)
it 'resigns if it took more than 10 min to get to send', ->
signHandler = helpers.createSpy('sign')
helpers.mockHttpResponse 200, {}, ['data']
request = makeRequest()
request.on('sign', signHandler)
request.build()
request.signedAt = new Date(request.signedAt - 60 * 12 * 1000)
request.send()
expect(signHandler.calls.length).to.equal(2)
describe 'httpData', ->
beforeEach ->
helpers.mockHttpResponse 200, {}, ['FOO', 'BAR', 'BAZ', 'QUX']
it 'emits httpData event on each chunk', ->
calls = []
request = makeRequest()
request.on('httpData', (chunk) -> calls.push(chunk.toString()))
request.send()
expect(calls).to.eql(['FOO', 'BAR', 'BAZ', 'QUX'])
it 'does not clear default httpData event if another is added', ->
request = makeRequest()
request.on('httpData', ->)
response = request.send()
expect(response.httpResponse.body.toString()).to.equal('FOOBARBAZQUX')
if AWS.util.isNode() and AWS.HttpClient.streamsApiVersion > 1
describe 'httpDownloadProgress', ->
beforeEach ->
helpers.mockHttpResponse 200, {'content-length': 12}, ['FOO', 'BAR', 'BAZ', 'QUX']
it 'emits httpDownloadProgress for each chunk', ->
progress = []
request = makeRequest()
request.on('httpDownloadProgress', (p) -> progress.push(p))
request.send()
expect(progress[0]).to.eql(loaded: 3, total: 12)
expect(progress[1]).to.eql(loaded: 6, total: 12)
expect(progress[2]).to.eql(loaded: 9, total: 12)
expect(progress[3]).to.eql(loaded: 12, total: 12)
describe 'retry', ->
it 'retries a request with a set maximum retries', ->
sendHandler = helpers.createSpy('send')
service.config.maxRetries = 10
helpers.mockHttpResponse
code: 'NetworkingError', message: 'Cannot connect'
request = makeRequest()
request.on('send', sendHandler)
response = request.send(->)
expect(retryHandler.calls.length).not.to.equal(0)
expect(errorHandler.calls.length).not.to.equal(0)
expect(completeHandler.calls.length).not.to.equal(0)
expect(successHandler.calls.length).to.equal(0)
expect(response.retryCount).to.equal(service.config.maxRetries)
expect(sendHandler.calls.length).to.equal(service.config.maxRetries + 1)
it 'retries with falloff', ->
helpers.mockHttpResponse
code: 'NetworkingError', message: 'Cannot connect'
makeRequest(->)
expect(delays).to.eql([30, 60, 120])
it 'uses retry from error.retryDelay property', ->
helpers.mockHttpResponse
code: 'NetworkingError', message: 'Cannot connect'
request = makeRequest()
request.on 'retry', (resp) -> resp.error.retryDelay = 17
response = request.send(->)
expect(delays).to.eql([17, 17, 17])
it 'retries if status code is >= 500', ->
helpers.mockHttpResponse 500, {}, ''
makeRequest (err) ->
expect(err.code).to.equal(500)
expect(err.message).to.equal(null)
expect(err.statusCode).to.equal(500)
expect(err.retryable).to.equal(true)
expect(@retryCount).
to.equal(service.config.maxRetries)
it 'should not emit error if retried fewer than maxRetries', ->
helpers.mockIntermittentFailureResponse 2, 200, {}, 'foo'
response = makeRequest(->)
expect(totalWaited).to.equal(90)
expect(response.retryCount).to.be.lessThan(service.config.maxRetries)
expect(response.data).to.equal('foo')
expect(errorHandler.calls.length).to.equal(0)
['ExpiredToken', 'ExpiredTokenException', 'RequestExpired'].forEach (name) ->
it 'invalidates expired credentials and retries', ->
helpers.spyOn(AWS.HttpClient, 'getInstance')
AWS.HttpClient.getInstance.andReturn handleRequest: (req, opts, cb, errCb) ->
if req.headers.Authorization.match('Credential=INVALIDKEY')
helpers.mockHttpSuccessfulResponse 403, {}, name, cb
else
helpers.mockHttpSuccessfulResponse 200, {}, 'DATA', cb
new AWS.SequentialExecutor()
creds =
numCalls: 0
expired: false
accessKeyId: 'INVALIDKEY'
secretAccessKey: 'INVALIDSECRET'
get: (cb) ->
if @expired
@numCalls += 1
@expired = false
@accessKeyId = 'VALIDKEY' + @numCalls
@secretAccessKey = 'VALIDSECRET' + @numCalls
cb()
service.config.credentials = creds
response = makeRequest(->)
expect(response.retryCount).to.equal(1)
expect(creds.accessKeyId).to.equal('VALIDKEY1')
expect(creds.secretAccessKey).to.equal('VALIDSECRET1')
[].forEach (code) ->
it 'attempts to redirect on ' + code + ' responses', ->
helpers.mockHttpResponse code, {location: 'http://redirected'}, ''
service.config.maxRetries = 0
service.config.maxRedirects = 5
response = makeRequest(->)
expect(response.request.httpRequest.endpoint.host).to.equal('redirected')
expect(response.error.retryable).to.equal(true)
expect(response.redirectCount).to.equal(service.config.maxRedirects)
expect(delays).to.eql([0, 0, 0, 0, 0])
it 'does not redirect if 3xx is missing location header', ->
helpers.mockHttpResponse 304, {}, ''
service.config.maxRetries = 0
response = makeRequest(->)
expect(response.request.httpRequest.endpoint.host).not.to.equal('redirected')
expect(response.error.retryable).to.equal(false)
describe 'success', ->
it 'emits success on a successful response', ->
helpers.mockHttpResponse 200, {}, 'Success!'
response = makeRequest(->)
expect(retryHandler.calls.length).to.equal(0)
expect(errorHandler.calls.length).to.equal(0)
expect(completeHandler.calls.length).not.to.equal(0)
expect(successHandler.calls.length).not.to.equal(0)
expect(response.retryCount).to.equal(0);
describe 'error', ->
it 'emits error if error found and should not be retrying', ->
helpers.mockHttpResponse 400, {}, ''
response = makeRequest(->)
expect(retryHandler.calls.length).not.to.equal(0)
expect(errorHandler.calls.length).not.to.equal(0)
expect(completeHandler.calls.length).not.to.equal(0)
expect(successHandler.calls.length).to.equal(0)
expect(response.retryCount).to.equal(0)
it 'emits error if an error is set in extractError', ->
error = code: 'ParseError', message: 'error message'
extractDataHandler = helpers.createSpy('extractData')
helpers.mockHttpResponse 400, {}, ''
request = makeRequest()
request.on('extractData', extractDataHandler)
request.on('extractError', (resp) -> resp.error = error)
response = request.send(->)
expect(response.error).to.equal(error)
expect(extractDataHandler.calls.length).to.equal(0)
expect(retryHandler.calls.length).not.to.equal(0)
expect(errorHandler.calls.length).not.to.equal(0)
expect(completeHandler.calls.length).not.to.equal(0)
describe 'logging', ->
data = null
logger = null
logfn = (d) -> data += d
match = /\[AWS mock 200 .* 0 retries\] mockMethod\(.*foo.*bar.*\)/
beforeEach ->
data = ''
logger = {}
service = new MockService(logger: logger)
it 'does nothing if logging is off', ->
service = new MockService(logger: null)
helpers.mockHttpResponse 200, {}, []
makeRequest().send()
expect(completeHandler.calls.length).not.to.equal(0)
it 'calls .log() on logger if it is available', ->
helpers.mockHttpResponse 200, {}, []
logger.log = logfn
makeRequest().send()
expect(data).to.match(match)
it 'calls .write() on logger if it is available', ->
helpers.mockHttpResponse 200, {}, []
logger.write = logfn
makeRequest().send()
expect(data).to.match(match)
describe 'terminal callback error handling', ->
describe 'without domains', ->
it 'emits uncaughtException', ->
helpers.mockHttpResponse 200, {}, []
expect(-> (makeRequest -> invalidCode)).to.throw()
expect(completeHandler.calls.length).not.to.equal(0)
expect(errorHandler.calls.length).to.equal(0)
expect(retryHandler.calls.length).to.equal(0)
['error', 'complete'].forEach (evt) ->
it 'raise exceptions from terminal ' + evt + ' events', ->
helpers.mockHttpResponse 500, {}, []
request = makeRequest()
expect(-> request.send(-> invalidCode)).to.throw()
expect(completeHandler.calls.length).not.to.equal(0)
if AWS.util.isNode()
describe 'with domains', ->
domains = []
createDomain = ->
domain = require('domain').create()
domains.push(domain)
domain
beforeEach ->
domains = []
afterEach ->
domains.forEach (d) -> d.exit(); d.dispose()
it 'sends error raised from complete event to a domain', ->
result = false
d = createDomain()
d.enter()
d.on('error', (e) -> result = e)
d.run ->
helpers.mockHttpResponse 200, {}, []
request = makeRequest()
request.on 'complete', -> invalidCode
expect(-> request.send()).not.to.throw()
expect(completeHandler.calls.length).not.to.equal(0)
expect(retryHandler.calls.length).to.equal(0)
expect(result.name).to.equal('ReferenceError')
d.exit()
it 'does not leak service error into domain', ->
result = false
d = createDomain()
d.on('error', (e) -> result = e)
d.enter()
d.run ->
helpers.mockHttpResponse 500, {}, []
makeRequest().send()
expect(completeHandler.calls.length).not.to.equal(0)
expect(result).to.equal(false)
d.exit()
it 'supports inner domains', (done) ->
helpers.mockHttpResponse 200, {}, []
err = new ReferenceError()
gotOuterError = false
gotInnerError = false
outerDomain = createDomain()
outerDomain.on 'error', -> gotOuterError = true
outerDomain.enter()
outerDomain.run ->
request = makeRequest()
innerDomain = createDomain()
innerDomain.enter()
innerDomain.add(request)
innerDomain.on 'error', ->
gotInnerError = true
expect(gotOuterError).to.equal(false)
expect(gotInnerError).to.equal(true)
expect(err.domainThrown).to.equal(false)
expect(err.domain).to.equal(innerDomain)
done()
request.send ->
innerDomain.run -> throw err