require_relative 'exchange_handler' require_relative '../core_ext/hash' require_relative '../not_found_errors' require_relative 'handler_accessors' require_relative '../interpreter' require 'forwardable' module Soaspec # Accessors specific to SOAP handler module SoapAccessors # Define attributes set on root SOAP element def root_attributes(attributes) define_method('request_root_attributes') do attributes end end end # Wraps around Savon client defining default values dependent on the soap request class SoapHandler < ExchangeHandler extend Soaspec::SoapAccessors extend Forwardable delegate [:operations] => :client # Savon client used to make SOAP calls attr_accessor :client # SOAP Operation to use by default attr_accessor :operation # Attributes set at the root XML element of SOAP request def request_root_attributes nil end # Options to log xml request and response def logging_options { log: true, # See request and response. (Put this in traffic file) log_level: :debug, logger: Soaspec::SpecLogger.create, pretty_print_xml: true # Prints XML pretty } end # Default Savon options. See http://savonrb.com/version2/globals.html for details # @return [Hash] Default Savon options for all BasicSoapHandler def default_options { ssl_verify_mode: :none, # Easier for testing. Not so secure follow_redirects: true, # Necessary for many API calls soap_version: 2, # use SOAP 1.2. You will get 415 error if this is incorrect raise_errors: false # HTTP errors not cause failure as often negative test scenarios expect not 200 response # Things could go wrong if not set properly # env_namespace: :soap, # Change environment namespace # namespace_identifier: :tst, # Change namespace element # element_form_default: :qualified # Populate each element with namespace # namespace: 'http://Extended_namespace.xsd' change root namespace # basic_auth: 'user', 'password' } end # Add values to here when extending this class to have default Savon options. # See http://savonrb.com/version2/globals.html for details # @return [Hash] Savon options adding to & overriding defaults def savon_options { } end # Setup object to handle communicating with a particular SOAP WSDL # @param [Hash] options Options defining SOAP request. WSDL, authentication, see http://savonrb.com/version2/globals.html for list of options def initialize(name = self.class.to_s, options = {}) if name.is_a?(Hash) && options == {} # If name is not set options = name name = self.class.to_s end super set_remove_key(options, :operation) set_remove_key(options, :default_hash) set_remove_key(options, :template_name) merged_options = Soaspec.log_api_traffic? ? default_options.merge(logging_options) : default_options merged_options.merge! savon_options merged_options.merge!(options) self.client = Savon.client(merged_options) end # Used in making request via hash or in template via Erb def request_body_params(request_parameters) test_values = request_parameters[:body] || request_parameters test_values.transform_keys_to_symbols if Soaspec.always_use_keys? end # Used in together with Exchange request that passes such override parameters # @param [Hash] request_parameters Parameters used to overwrite defaults in request def make_request(request_parameters) test_values = request_body_params request_parameters # Call the SOAP operation with the request XML provided begin if @request_option == :template test_values = IndifferentHash.new(test_values) # Allow test_values to be either Symbol or String client.call(operation, xml: Soaspec::TemplateReader.new.render_body(template_name, binding)) elsif @request_option == :hash client.call(operation, message: @default_hash.merge(test_values), attributes: request_root_attributes) end rescue Savon::HTTPError => soap_error soap_error end end # @param [Hash] format Format of expected result # @return [Object] Generic body to be displayed in error messages def response_body(response, format: :hash) case format when :hash response.body when :raw response.xml else response.body end end # @return [Boolean] Whether the request found the desired value or not def found?(response) status_code_for(response) != 404 end # Response status code for response. '200' indicates a success # @param [Savon::Response] response # @return [Integer] Status code def status_code_for(response) response.http.code end # @return [Boolean] Whether response includes provided string within it def include_in_body?(response, expected) response.to_xml.to_s.include? expected end # @param [Symbol] expected # @return [Boolean] Whether response body contains expected key def include_key?(response, expected) body = response.body body.extend Hashie::Extensions::DeepFind !body.deep_find_all(expected).empty? end # Convert all XML nodes to lowercase # @param [Nokogiri::XML::Document] def convert_to_lower_case(xml_doc) xml_doc.traverse do |node| node.name = node.name.downcase if node.kind_of?(Nokogiri::XML::Element) end end # Returns the value at the provided xpath # @param [Savon::Response] response # @param [String] xpath # @return [Enumerable] Elements found through Xpath def xpath_elements_for(response: nil, xpath: nil, attribute: nil) raise ArgumentError('response and xpath must be passed to method') unless response && xpath xpath = "//*[@#{attribute}]" unless attribute.nil? xpath = '//' + xpath if xpath[0] != '/' temp_doc = response.doc.dup convert_to_lower_case(temp_doc) if convert_to_lower? if strip_namespaces? && !xpath.include?(':') temp_doc.remove_namespaces! temp_doc.xpath(xpath) else temp_doc.xpath(xpath, temp_doc.collect_namespaces) end end # Based on a exchange, return the value at the provided xpath # If the path does not begin with a '/', a '//' is added to it # @param [Savon::Response] response # @param [String] path Xpath # @param [String] attribute Generic attribute to find. Will override path # @return [String] Value at Xpath def value_from_path(response, path, attribute: nil) results = xpath_elements_for(response: response, xpath: path, attribute: attribute) raise NoElementAtPath, "No value at Xpath '#{path}' in XML #{response.doc}" if results.empty? return results.first.inner_text if attribute.nil? results.first.attributes[attribute].inner_text end # @return [Enumerable] List of values returned from path def values_from_path(response, path, attribute: nil) xpath_elements_for(response: response, xpath: path, attribute: attribute).map(&:inner_text) end # alias elements xpath_elements_for # @return [Boolean] Whether any of the keys of the Body Hash include value def include_value?(response, expected_value) response.body.include_value?(expected_value) end # Hash of response body def to_hash(response) response.body end # Convenience methods for once off usage of a SOAP request class << self # 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) tmp_class = new(method_name) operations = tmp_class.operations if operations.include? method_name tmp_class.operation = method_name exchange = Exchange.new(method_name, *args) exchange.exchange_handler = tmp_class yield exchange if block_given? exchange else super end end def respond_to_missing?(method_name, *args) tmp_class = new(args) operations = tmp_class.operations operations.include?(method_name) || super end end end end