require_relative '../soaspec' require_relative 'exchange_properties' # This represents a request / response pair # Essentially, params in the exchange that are set are related to the request # What is returned is related to the response class Exchange extend ExchangeProperties # Instance of ExchangeHandler for which this exchange is made attr_accessor :exchange_handler # How many times to retry for a success attr_accessor :retry_count # Name used for displaying class attr_accessor :test_name # Expect Factory to fail upon trying to create attr_writer :fail_factory # Parameters to override for default params attr_accessor :override_parameters def values_from_path(path, attribute: nil) exchange_handler.values_from_path(response, path, attribute: attribute) end # Set retry for success variable to true so that request will be retried # for retry_count until it's true def retry_for_success @retry_for_success = true self end # @return [Bool] Whether to keep making request until success code reached def retry_for_success? @retry_for_success end # @return [Boolean] Soaspec::ExchangeHandler used by this exchange def default_handler_used nil end # @param [String] element Element to define methods for def methods_for_element(element) element_name = element.to_s.split('__custom_path_').last define_singleton_method(element_name) do exchange_handler.__send__(element, response) # Forward the call onto handler to retrieve the element for the response end define_singleton_method("#{element_name}?") do begin __send__ element_name true rescue NoElementAtPath false end end end # @param [Symbol, String] name Name shown in RSpec run # @param [Hash] override_parameters Parameters to override for default params def initialize(name = self.class.to_s, override_parameters = {}) self.test_name ||= name.to_s # As a last resort this uses the global parameter. The handler should be set straight before an exchange is made to use this @exchange_handler ||= default_handler_used || Soaspec.api_handler raise '@exchange_handler not set. Set either with `Soaspec.api_handler = Handler.new` or within the exchange' unless @exchange_handler @fail_factory = nil @override_parameters = override_parameters @retry_for_success = false self.retry_count = 3 @exchange_handler.elements.each { |element| methods_for_element(element) } end # Specify a url to add onto the base_url of the ExchangeHandler used # @param [String] url Url to add onto the base_url of the ExchangeHandler used def suburl=(url) @override_parameters[:suburl] = url end # Specify HTTP method to use. Default is :post # @param [Symbol] method HTTP method. E.g, :get, :patch def method=(method) @override_parameters[:method] = method end # Make request to handler with parameters defined # Will retry until success code reached if retry_for_success? is set # @return [Response] Response from Api handler def make_request Soaspec::SpecLogger.info 'Example ' + test_name request_params = @override_parameters (1..retry_count).each do |count| response = exchange_handler.make_request(request_params) return response unless retry_for_success? return response if (200..299).cover? @exchange_handler.status_code_for(response) sleep 0.5 break response if count == retry_count end end # Stores a value in the api handler that can be accessed by the provided name # @param [Symbol] name Name of method to use to access this value within handler # @param [String] value Path to value to store def store(name, value) exchange_handler.store(name, self[value]) end # Retrieve the stored value from the Api Handler # @param [String, Symbol] name Name of value to retrieve # @return [Object] value from the Api Handler stored previously def retrieve(name) method = '__stored_val__' + name.to_s raise ArgumentError('Value not stored at ') unless exchange_handler.respond_to? method exchange_handler.send(method) end # Name describing this class when used with `RSpec.describe` # This will make the request and store the response # @return [String] Name given when initializing def to_s test_name end # Returns response object from Api. Will make the request if not made and then cache it for later on # @example For SOAP it will be a Savon response # response.body (body of response as Hash) # response.header (head of response as Hash) # @example For REST it will be a RestClient::Response def response Soaspec.last_exchange = self @response ||= make_request @response.define_singleton_method(:exchange) { Soaspec.last_exchange } unless @response.respond_to?(:exchange) @response end alias call response # Request of API call. Either intended request or actual request def request exchange_handler.request(@response) end # Get status code from api class. This is http response for Web Api # @return [Integer] Status code from api class def status_code exchange_handler.status_code_for(response) end # Dummy request used to make a request without verifying it and ignoring WSDL errors # @return [Boolean] Always returns true. Unless of course an unexpected exception occurs def dummy_request make_request true rescue Savon::HTTPError puts 'Resolver error' # This seems to occur first time IP address asks for WSDL true end # @return [Boolean] Whether an element exists at the path def element?(path) [path] true rescue NoElementAtPath false end # Extract value from path api class # @param [Object] path Path to return element for api class E.g - for SOAP this is XPath string. For JSON, this is Hash dig Array # @return [String] Value at path def [](path) exchange_handler.value_from_path(response, path.to_s) end # Set a parameter request in the request body. # Can be used to build a request over several steps (e.g Cucumber) # Will be used with FactoryBot def []=(key, value) @override_parameters[:body] ||= {} @override_parameters[:body][key] = value end # Implement undefined setter with []= for FactoryBot to use without needing to define params to set # @param [Object] method_name Name of method not defined # @param [Object] args Arguments passed to method # @param [Object] block def method_missing(method_name, *args, &block) set_value = args.first if method_name[-1] == '=' # A setter method getter_name = method_name[0..-2] if set_value.class < Exchange # This would be prerequisite exchange define_singleton_method(getter_name) do set_value end self[getter_name] = set_value.id if set_value.respond_to?(:id) else self[getter_name] = set_value end else super end end # Used for setters that are not defined def respond_to_missing?(method_name, *args) method_name[-1] == '=' || super end # Makes request, caching the response and returning self # Used by FactoryBot # @return [Self] def save! @retry_for_success = @fail_factory ? false : true call self end # @return [Hash] Hash representing the response of the API def to_hash exchange_handler.to_hash(response) end # Wait until the passed block returns true # @param [Hash] opts Options for this instance # @option opts [Numeric] :timeout (5) Seconds to wait before timing out. # @option opts [Numeric] :interval (0.2) Seconds to sleep between polls. # @option opts [String] :message Exception mesage if timed out. # @option opts [Array, Exception] :ignore Exceptions to ignore while polling (default: Error::NoSuchElementError) # @return [Self] Returns itself so operations can be done on the exchange after it's done waiting def until(opts = {}, &script) Soaspec::Wait.until(opts) do @response = nil # Reset response so it can be made repeatedly instance_eval(&script) end self end end