lib/soaspec/exchange_handlers/soap_handler.rb in soaspec-0.2.32 vs lib/soaspec/exchange_handlers/soap_handler.rb in soaspec-0.2.33

- old
+ new

@@ -1,241 +1,241 @@ -# frozen_string_literal: true - -require_relative 'exchange_handler' -require_relative '../core_ext/hash' -require_relative '../errors' -require_relative 'handler_accessors' -require_relative '../interpreter' -require_relative 'request/soap_request' -require 'forwardable' - -module Soaspec - # Accessors specific to SOAP handler - module SoapAccessors - # Define attributes set on root SOAP element - # @param [Hash] attributes Attributes used in the root SOAP element - def root_attributes(attributes) - define_method('request_root_attributes') { attributes } - 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; 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 - # @example 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' - # @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 - } - 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 [String] name Name to describe handler. Used in calling 'to_s' - # @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_keys(options, %i[operation default_hash template_name]) - merged_options = Soaspec::SpecLogger.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 - - # @param [Hash] override_parameters Parameters for building the request - # @return [Hash] Parameters used in making a request - def request_parameters(override_parameters) - SoapRequest.new(operation, request_body_params(override_parameters), @request_option) - end - - # Used in making request via hash or in template via Erb - # @param [Hash] request_parameters Hash representing elements to send in request - # If the :body key is set, this will be used as the request body - def request_body_params(request_parameters) - test_values = request_parameters[:body] || request_parameters - test_values.transform_keys_to_symbols if Soaspec.always_use_keys? - if @request_option == :template - test_values = IndifferentHash.new(test_values) # Allow test_values to be either Symbol or String - { xml: Soaspec::TemplateReader.new.render_body(template_name, binding) } - elsif @request_option == :hash - { message: @default_hash.merge(test_values), attributes: request_root_attributes } - end - 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) - # Call the SOAP operation with the request XML provided - request = request_parameters(request_parameters) - begin - client.call request.operation, request.body # request_body_params(request_parameters) - rescue Savon::HTTPError => e - 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] xml_doc Xml document to convert - def convert_to_lower_case(xml_doc) - xml_doc.traverse do |node| - node.name = node.name.downcase if node.is_a?(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 +# frozen_string_literal: true + +require_relative 'exchange_handler' +require_relative '../core_ext/hash' +require_relative '../errors' +require_relative 'handler_accessors' +require_relative '../interpreter' +require_relative 'request/soap_request' +require 'forwardable' + +module Soaspec + # Accessors specific to SOAP handler + module SoapAccessors + # Define attributes set on root SOAP element + # @param [Hash] attributes Attributes used in the root SOAP element + def root_attributes(attributes) + define_method('request_root_attributes') { attributes } + 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; 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 + # @example 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' + # @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 + } + 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 [String] name Name to describe handler. Used in calling 'to_s' + # @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_keys(options, %i[operation default_hash template_name]) + merged_options = Soaspec::SpecLogger.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 + + # @param [Hash] override_parameters Parameters for building the request + # @return [Hash] Parameters used in making a request + def request_parameters(override_parameters) + SoapRequest.new(operation, request_body_params(override_parameters), @request_option) + end + + # Used in making request via hash or in template via Erb + # @param [Hash] request_parameters Hash representing elements to send in request + # If the :body key is set, this will be used as the request body + def request_body_params(request_parameters) + test_values = request_parameters[:body] || request_parameters + test_values.transform_keys_to_symbols if Soaspec.always_use_keys? + if @request_option == :template + test_values = IndifferentHash.new(test_values) # Allow test_values to be either Symbol or String + { xml: Soaspec::TemplateReader.new.render_body(template_name, binding) } + elsif @request_option == :hash + { message: @default_hash.merge(test_values), attributes: request_root_attributes } + end + 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) + # Call the SOAP operation with the request XML provided + request = request_parameters(request_parameters) + begin + client.call request.operation, request.body # request_body_params(request_parameters) + rescue Savon::HTTPError => e + 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] xml_doc Xml document to convert + def convert_to_lower_case(xml_doc) + xml_doc.traverse do |node| + node.name = node.name.downcase if node.is_a?(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