Skip to content
This repository has been archived by the owner on Nov 20, 2018. It is now read-only.

Commit

Permalink
Merge pull request #123 from aws/stub-responses
Browse files Browse the repository at this point in the history
Added support for stubbing client responses.
  • Loading branch information
trevorrowe committed Oct 1, 2014
2 parents c7cd086 + bee8f3b commit 37d35dc
Show file tree
Hide file tree
Showing 8 changed files with 557 additions and 6 deletions.
2 changes: 2 additions & 0 deletions aws-sdk-core/lib/aws-sdk-core.rb
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ module Aws
end

autoload :Client, 'aws-sdk-core/client'
autoload :ClientStubs, 'aws-sdk-core/client_stubs'
autoload :CredentialProviderChain, 'aws-sdk-core/credential_provider_chain'
autoload :Credentials, 'aws-sdk-core/credentials'
autoload :EmptyStructure, 'aws-sdk-core/empty_structure'
Expand Down Expand Up @@ -137,6 +138,7 @@ module Plugins
autoload :S3SseCpk, 'aws-sdk-core/plugins/s3_sse_cpk'
autoload :S3UrlEncodedKeys, 'aws-sdk-core/plugins/s3_url_encoded_keys'
autoload :SQSQueueUrls, 'aws-sdk-core/plugins/sqs_queue_urls'
autoload :StubResponses, 'aws-sdk-core/plugins/stub_responses'
autoload :SWFReadTimeouts, 'aws-sdk-core/plugins/swf_read_timeouts'
autoload :UserAgent, 'aws-sdk-core/plugins/user_agent'

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ module ServiceCustomizations
'Aws::Plugins::RegionalEndpoint',
'Aws::Plugins::ResponsePaging',
'Aws::Plugins::RequestSigner',
'Aws::Plugins::StubResponses',
]

@customizations = Hash.new {|h,k| h[k] = [] }
Expand Down
3 changes: 2 additions & 1 deletion aws-sdk-core/lib/aws-sdk-core/client.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
module Aws

# Base class for all {Aws} service clients.
class Client < Seahorse::Client::Base

include ClientStubs

# Waits until a particular condition is satisfied. This works by
# polling a client request and checking for particular response
# data or errors. Waiters each have a default duration max attempts
Expand Down
227 changes: 227 additions & 0 deletions aws-sdk-core/lib/aws-sdk-core/client_stubs.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
module Aws

# This module provides the ability to specify the data and/or errors to
# return when a client is using stubbed responses. Pass
# `:stub_responses => true` to a client constructor to enable this
# behavior.
module ClientStubs

def initialize(*args)
@stubs = {}
super
end

# Configures what data / errors should be returned from the named operation
# when response stubbing is enabled.
#
# ## Basic usage
#
# By default, fake responses are generated. You can override the default
# fake data with specific response data by passing a hash.
#
# # enable response stubbing in the client constructor
# client = Aws::S3::Client.new(stub_responses: true)
#
# # specify the response data for #list_buckets
# client.stub_responses(:list_buckets, buckets:[{name:'aws-sdk'}])
#
# # no api calls made, stub returned
# client.list_buckets.map(&:name)
# #=> ['aws-sdk']
#
# ## Stubbing Errors
#
# When stubbing is enabled, the SDK will default to generate
# fake responses with placeholder values. You can override the data
# returned. You can also specify errors it should raise.
#
# client.stub_responses(:get_object, 'NotFound')
# client.get_object(bucket:'aws-sdk', key:'foo')
# #=> raises Aws::S3::Errors::NotFound
#
# client.stub_responses(:get_object, Timeout::Error)
# client.get_object(bucket:'aws-sdk', key:'foo')
# #=> raises new Timeout::Error
#
# client.stub_responses(:get_object, RuntimeError.new('custom message'))
# client.get_object(bucket:'aws-sdk', key:'foo')
# #=> raises the given runtime error object
#
# ## Stubbing Multiple Responses
#
# Calling an operation multiple times will return similar responses.
# You can configure multiple stubs and they will be returned in sequence.
#
#
# client.stub_responses(:head_object, [
# 'NotFound',
# { content_length: 150 },
# ])
#
# client.head_object(bucket:'aws-sdk', key:'foo')
# #=> raises Aws::S3::Errors::NotFound
#
# resp = client.head_object(bucket:'aws-sdk', key:'foo')
# resp.content_length #=> 150
#
# @param [Symbol] operation_name
# @param [Mixed] stubs One or more responses to return from the named
# operation.
# @return [void]
# @raise [RuntimeError] Raises a runtime error when {#stub} is called
# on a client that has not enabled response stubbing via
# `:stub_responses => true`.
def stub_responses(operation_name, *stubs)
if config.stub_responses
apply_stubs(operation_name, stubs.flatten)
else
msg = 'stubbing is not enabled; enable stubbing in the constructor '
msg << 'with `:stub_responses => true`'
raise msg
end
end

# @api private
def next_stub(operation_name)
stubs = @stubs[operation_name.to_sym] || []
case stubs.length
when 0 then new_stub(operation_name)
when 1 then stubs.first
else stubs.shift
end
end

private

# @param [Symbol] operation_name
# @param [Hash, nil] data
# @return [Structure]
def new_stub(operation_name, data = nil)
Stub.new(operation(operation_name).output).format(data || {})
end

def apply_stubs(operation_name, stubs)
@stubs[operation_name.to_sym] = stubs.map do |stub|
case stub
when Exception then stub
when String then service_error_class(stub)
when Hash then new_stub(operation_name, stub)
else stub
end
end
end

def service_error_class(name)
svc_module = Aws.const_get(self.class.name.split('::')[1])
svc_module.const_get(:Errors).const_get(name)
end

class Stub

# @param [Seahorse::Models::Shapes::Structure] output_shape This should
# be the output shape for an operation.
def initialize(output_shape)
@shape = output_shape
end

# @param [Hash] data An optional hash of data to format into the stubbed
# object.
def format(data = {})
if @shape.nil?
empty_stub(data)
else
validate_data(data)
stub(@shape, data)
end
end

private

def stub(shape, value)
case shape
when Seahorse::Model::Shapes::Structure then stub_structure(shape, value)
when Seahorse::Model::Shapes::List then stub_list(shape, value || [])
when Seahorse::Model::Shapes::Map then stub_map(shape, value || {})
else stub_scalar(shape, value)
end
end

def stub_structure(shape, hash)
if hash
structure_obj(shape, hash)
else
nil
end
end

def structure_obj(shape, hash)
stubs = Structure.new(shape.member_names)
shape.members.each do |member_name, member_shape|
value = structure_value(shape, member_name, member_shape, hash)
stubs[member_name] = stub(member_shape, value)
end
stubs
end

def structure_value(shape, member_name, member_shape, hash)
if hash.key?(member_name)
hash[member_name]
elsif
Seahorse::Model::Shapes::Structure === member_shape &&
shape.required.include?(member_name)
then
{}
else
nil
end
end

def stub_list(shape, array)
stubs = []
array.each do |value|
stubs << stub(shape.member, value)
end
stubs
end

def stub_map(shape, value)
stubs = {}
value.each do |key, value|
stubs[key] = stub(shape.value, value)
end
stubs
end

def stub_scalar(shape, value)
if value.nil?
case shape
when Seahorse::Model::Shapes::String then shape.name
when Seahorse::Model::Shapes::Integer then 0
when Seahorse::Model::Shapes::Float then 0.0
when Seahorse::Model::Shapes::Boolean then false
when Seahorse::Model::Shapes::Timestamp then Time.now
else nil
end
else
value
end
end

def empty_stub(data)
if data.empty?
Structure.new(data)
else
msg = 'unable to generate a stubbed response from the given data; '
msg << 'this operation does not return data'
raise ArgumentError, msg
end
end

def validate_data(data)
args = [@shape, { validate_required:false }]
Seahorse::Client::ParamValidator.new(*args).validate!(data)
end

end
end
end
51 changes: 51 additions & 0 deletions aws-sdk-core/lib/aws-sdk-core/plugins/stub_responses.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
module Aws
module Plugins

# @seahorse.client.option [Boolean] :stub_responses (false)
# Causes the client to return stubbed responses. By default
# fake responses are generated and returned. You can specify
# the response data to return or errors to raise by calling
# {ClientStubs#stub_responses}. See {ClientStubs} for more information.
#
# ** Please note ** When response stubbing is enabled, no HTTP
# requests are made, and retries are disabled.
#
class StubResponses < Seahorse::Client::Plugin

option(:stub_responses, false)

def add_handlers(handlers, config)
handlers.add(Handler, step: :send) if config.stub_responses
end

def after_initialize(client)
# disable retries when stubbing responses
client.config.retry_limit = 0 if client.config.stub_responses
end

class Handler < Seahorse::Client::Handler

def call(context)
response = Seahorse::Client::Response.new(context: context)
apply_stub(response, context.client.next_stub(context.operation_name))
response
end

private

def apply_stub(resp, stub)
if Exception === stub
resp.error = stub
elsif Class === stub && stub.ancestors.include?(Errors::ServiceError)
resp.error = stub.new(resp.context, 'stubbed error')
elsif Class === stub && stub.ancestors.include?(Exception)
resp.error = stub.new
else
resp.data = stub
end
end

end
end
end
end
14 changes: 9 additions & 5 deletions aws-sdk-core/lib/seahorse/client/param_validator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ def self.validate!(shape, params)
end

# @param [Model::Shapes::Shape] shape
def initialize(shape)
# @option options [Boolean] :validate_required (true)
def initialize(shape, options = {})
@shape = shape || Seahorse::Model::Shapes::Structure.new
@validate_required = options[:validate_required] != false
end

# @param [Hash] params
Expand All @@ -29,10 +31,12 @@ def structure(structure, values, errors, context)
return unless hash?(values, errors, context)

# ensure required members are present
structure.required.each do |member_name|
if values[member_name].nil?
param = "#{context}[#{member_name.inspect}]"
errors << "missing required parameter #{param}"
if @validate_required
structure.required.each do |member_name|
if values[member_name].nil?
param = "#{context}[#{member_name.inspect}]"
errors << "missing required parameter #{param}"
end
end
end

Expand Down
Loading

0 comments on commit 37d35dc

Please sign in to comment.