lib/soaspec/exchange_handlers/soap_handler.rb in soaspec-0.0.81 vs lib/soaspec/exchange_handlers/soap_handler.rb in soaspec-0.0.82

- old
+ new

@@ -1,232 +1,232 @@ - -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 = {}) - @default_hash = {} - @request_option = :hash - 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 = default_options.merge logging_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[: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 - begin - if @request_option == :template - request_body = File.read('template/' + template_name + '.xml') - render_body = ERB.new(request_body).result(binding) - @client.call(operation, xml: render_body) # Call the SOAP operation with the request XML provided - elsif @request_option == :hash - @client.call(operation, message: @default_hash.merge(test_values), attributes: request_root_attributes) - end - rescue Savon::HTTPError => e - e - 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 Soaspec.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 - - # 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 - 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 + +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 = {}) + @default_hash = {} + @request_option = :hash + 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 = default_options.merge logging_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[: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 + begin + if @request_option == :template + request_body = File.read('template/' + template_name + '.xml') + render_body = ERB.new(request_body).result(binding) + @client.call(operation, xml: render_body) # Call the SOAP operation with the request XML provided + elsif @request_option == :hash + @client.call(operation, message: @default_hash.merge(test_values), attributes: request_root_attributes) + end + rescue Savon::HTTPError => e + e + 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 Soaspec.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 + + # 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 + 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 \ No newline at end of file