loading
Generated 2022-07-14T06:25:27+00:00

All Files ( 98.46% covered at 187.41 hits/line )

18 files in total.
911 relevant lines, 897 lines covered and 14 lines missed. ( 98.46% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line
lib/savon.rb 100.00 % 28 16 16 0 28.56
lib/savon/block_interface.rb 92.31 % 27 13 12 1 9.23
lib/savon/builder.rb 92.99 % 297 157 146 11 628.89
lib/savon/client.rb 100.00 % 93 47 47 0 54.66
lib/savon/header.rb 100.00 % 89 47 47 0 76.96
lib/savon/http_error.rb 100.00 % 27 12 12 0 29.58
lib/savon/log_message.rb 100.00 % 53 29 29 0 5.83
lib/savon/message.rb 100.00 % 38 19 19 0 71.95
lib/savon/mock.rb 100.00 % 6 3 3 0 1.00
lib/savon/mock/expectation.rb 97.67 % 81 43 42 1 6.23
lib/savon/model.rb 100.00 % 85 33 33 0 8.45
lib/savon/operation.rb 100.00 % 147 72 72 0 65.32
lib/savon/options.rb 100.00 % 483 177 177 0 212.30
lib/savon/qualified_message.rb 100.00 % 52 31 31 0 30.94
lib/savon/request.rb 98.53 % 113 68 67 1 181.51
lib/savon/request_logger.rb 100.00 % 50 27 27 0 34.30
lib/savon/response.rb 100.00 % 161 88 88 0 50.28
lib/savon/soap_fault.rb 100.00 % 49 29 29 0 64.10

lib/savon.rb

100.0% lines covered

16 relevant lines. 16 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module Savon
  3. 1 Error = Class.new(RuntimeError)
  4. 1 InitializationError = Class.new(Error)
  5. 1 UnknownOptionError = Class.new(Error)
  6. 1 UnknownOperationError = Class.new(Error)
  7. 1 InvalidResponseError = Class.new(Error)
  8. 1 def self.client(globals = {}, &block)
  9. 156 Client.new(globals, &block)
  10. end
  11. 1 def self.observers
  12. 139 @observers ||= []
  13. end
  14. 1 def self.notify_observers(operation_name, builder, globals, locals)
  15. 131 observers.inject(nil) do |response, observer|
  16. 19 observer.notify(operation_name, builder, globals, locals)
  17. end
  18. end
  19. end
  20. 1 require "savon/version"
  21. 1 require "savon/client"
  22. 1 require "savon/model"

lib/savon/block_interface.rb

92.31% lines covered

13 relevant lines. 12 lines covered and 1 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module Savon
  3. 1 class BlockInterface
  4. 1 def initialize(target)
  5. 32 @target = target
  6. end
  7. 1 def evaluate(block)
  8. 32 if block.arity > 0
  9. 23 block.call(@target)
  10. else
  11. 9 @original = eval("self", block.binding)
  12. 9 instance_eval(&block)
  13. end
  14. end
  15. 1 private
  16. 1 def method_missing(method, *args, &block)
  17. 9 @target.send(method, *args, &block)
  18. rescue NoMethodError
  19. @original.send(method, *args, &block)
  20. end
  21. end
  22. end

lib/savon/builder.rb

92.99% lines covered

157 relevant lines. 146 lines covered and 11 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "savon/header"
  3. 1 require "savon/message"
  4. 1 require "nokogiri"
  5. 1 require "builder"
  6. 1 require "gyoku"
  7. 1 module Savon
  8. 1 class Builder
  9. 1 attr_reader :multipart
  10. 1 SCHEMA_TYPES = {
  11. "xmlns:xsd" => "http://www.w3.org/2001/XMLSchema",
  12. "xmlns:xsi" => "http://www.w3.org/2001/XMLSchema-instance"
  13. }
  14. 1 SOAP_NAMESPACE = {
  15. 1 => "http://schemas.xmlsoap.org/soap/envelope/",
  16. 2 => "http://www.w3.org/2003/05/soap-envelope"
  17. }
  18. 1 WSA_NAMESPACE = "http://www.w3.org/2005/08/addressing"
  19. 1 def initialize(operation_name, wsdl, globals, locals)
  20. 165 @operation_name = operation_name
  21. 165 @wsdl = wsdl
  22. 165 @globals = globals
  23. 165 @locals = locals
  24. 165 @signature = @locals[:wsse_signature] || @globals[:wsse_signature]
  25. 165 @types = convert_type_definitions_to_hash
  26. 165 @used_namespaces = convert_type_namespaces_to_hash
  27. end
  28. 1 def pretty
  29. 1 Nokogiri.XML(to_s).to_xml(:indent => 2)
  30. end
  31. 1 def build_document
  32. 133 xml_result = build_xml
  33. # if we have a signature sign the document
  34. 133 if @signature
  35. 5 @signature.document = xml_result
  36. 5 2.times do
  37. 10 @header = nil
  38. 10 @signature.document = build_xml
  39. end
  40. 5 xml_result = @signature.document
  41. end
  42. # if there are attachments for the request, we should build a multipart message according to
  43. # https://www.w3.org/TR/SOAP-attachments
  44. 133 if @locals[:attachments]
  45. 3 build_multipart_message(xml_result)
  46. else
  47. 130 xml_result
  48. end
  49. end
  50. 1 def header_attributes
  51. 32 @globals[:use_wsa_headers] ? { 'xmlns:wsa' => WSA_NAMESPACE } : {}
  52. end
  53. 1 def body_attributes
  54. 144 @body_attributes ||= @signature.nil? ? {} : @signature.body_attributes
  55. end
  56. 1 def to_s
  57. 147 return @locals[:xml] if @locals.include? :xml
  58. 133 build_document
  59. end
  60. 1 private
  61. 1 def convert_type_definitions_to_hash
  62. 165 @wsdl.type_definitions.inject({}) do |memo, (path, type)|
  63. 2353 memo[path] = type
  64. 2353 memo
  65. end
  66. end
  67. 1 def convert_type_namespaces_to_hash
  68. 165 @wsdl.type_namespaces.inject({}) do |memo, (path, uri)|
  69. 7169 key, value = use_namespace(path, uri)
  70. 7169 memo[key] = value
  71. 7169 memo
  72. end
  73. end
  74. 1 def use_namespace(path, uri)
  75. 7169 @internal_namespace_count ||= 0
  76. 7169 unless identifier = namespace_by_uri(uri)
  77. 2 identifier = "ins#{@internal_namespace_count}"
  78. 2 namespaces["xmlns:#{identifier}"] = uri
  79. 2 @internal_namespace_count += 1
  80. end
  81. 7169 [path, identifier]
  82. end
  83. 1 def namespaces_with_globals
  84. 143 namespaces.merge @globals[:namespaces]
  85. end
  86. 1 def namespaces
  87. 7314 @namespaces ||= begin
  88. 145 namespaces = SCHEMA_TYPES.dup
  89. # check namespace_identifier
  90. 145 namespaces["xmlns#{namespace_identifier.nil? ? '' : ":#{namespace_identifier}"}"] =
  91. @globals[:namespace] || @wsdl.namespace
  92. # check env_namespace
  93. 145 namespaces["xmlns#{env_namespace && env_namespace != "" ? ":#{env_namespace}" : ''}"] =
  94. SOAP_NAMESPACE[@globals[:soap_version]]
  95. 145 if @wsdl&.document
  96. 129 @wsdl.parser.namespaces.each do |identifier, path|
  97. 726 next if namespaces.key?("xmlns:#{identifier}")
  98. 487 namespaces["xmlns:#{identifier}"] = path
  99. end
  100. end
  101. 145 namespaces
  102. end
  103. end
  104. 1 def env_namespace
  105. 1389 @env_namespace ||= @globals[:env_namespace] || :env
  106. end
  107. 1 def header
  108. 175 @header ||= Header.new(@globals, @locals)
  109. end
  110. 1 def namespaced_message_tag
  111. 142 tag_name = message_tag
  112. 142 return [tag_name] if @wsdl.document? and @wsdl.soap_input(@operation_name.to_sym).is_a?(Hash)
  113. 142 if namespace_identifier == nil
  114. 1 [tag_name, message_attributes]
  115. 141 elsif @used_namespaces[[tag_name.to_s]]
  116. 118 [@used_namespaces[[tag_name.to_s]], tag_name, message_attributes]
  117. else
  118. 23 [namespace_identifier, tag_name, message_attributes]
  119. end
  120. end
  121. 1 def serialized_message_tag
  122. [:wsdl, @wsdl.soap_input(@operation_name.to_sym).keys.first, {}]
  123. end
  124. 1 def serialized_messages
  125. messages = ""
  126. message_tag = serialized_message_tag[1]
  127. @wsdl.soap_input(@operation_name.to_sym)[message_tag].each_pair do |message, type|
  128. break if @locals[:message].nil?
  129. message_locals = @locals[:message][message.snakecase.to_sym]
  130. message_content = Message.new(message_tag, namespace_identifier, @types, @used_namespaces, message_locals, :unqualified, @globals[:convert_request_keys_to], @globals[:unwrap]).to_s
  131. messages += "<#{message} xsi:type=\"#{type.join(':')}\">#{message_content}</#{message}>"
  132. end
  133. messages
  134. end
  135. 1 def message_tag
  136. 285 wsdl_tag_name = @wsdl.document? && @wsdl.soap_input(@operation_name.to_sym)
  137. 285 message_tag = wsdl_tag_name.keys.first if wsdl_tag_name.is_a?(Hash)
  138. 285 message_tag ||= @locals[:message_tag]
  139. 285 message_tag ||= wsdl_tag_name
  140. 285 message_tag ||= Gyoku.xml_tag(@operation_name, :key_converter => @globals[:convert_request_keys_to])
  141. 285 message_tag.to_sym
  142. end
  143. 1 def message_attributes
  144. 142 @locals[:attributes] || {}
  145. end
  146. 1 def body_message
  147. 142 if @wsdl.document? and @wsdl.soap_input(@operation_name.to_sym).is_a?(Hash)
  148. serialized_messages
  149. else
  150. 142 message.to_s
  151. end
  152. end
  153. 1 def message
  154. 143 element_form_default = @globals[:element_form_default] || @wsdl.element_form_default
  155. # TODO: clean this up! [dh, 2012-12-17]
  156. 143 Message.new(message_tag, namespace_identifier, @types, @used_namespaces, @locals[:message],
  157. element_form_default, @globals[:convert_request_keys_to], @globals[:unwrap])
  158. end
  159. 1 def namespace_identifier
  160. 597 return @globals[:namespace_identifier] if @globals.include? :namespace_identifier
  161. 586 return @namespace_identifier if @namespace_identifier
  162. 142 operation = @wsdl.operations[@operation_name] if @wsdl.document?
  163. 142 namespace_identifier = operation[:namespace_identifier] if operation
  164. 142 namespace_identifier ||= "wsdl"
  165. 142 @namespace_identifier = namespace_identifier.to_sym
  166. end
  167. 1 def namespace_by_uri(uri)
  168. 7169 namespaces.each do |candidate_identifier, candidate_uri|
  169. 23304 return candidate_identifier.gsub(/^xmlns:/, '') if candidate_uri == uri
  170. end
  171. nil
  172. end
  173. 1 def builder
  174. 143 builder = ::Builder::XmlMarkup.new
  175. 143 builder.instruct!(:xml, :encoding => @globals[:encoding])
  176. 143 builder
  177. end
  178. 1 def tag(xml, name, namespaces = {}, &block)
  179. 318 if env_namespace && env_namespace != ""
  180. 318 xml.tag! env_namespace, name, namespaces, &block
  181. else
  182. xml.tag! name, namespaces, &block
  183. end
  184. end
  185. 1 def build_xml
  186. 143 tag(builder, :Envelope, namespaces_with_globals) do |xml|
  187. 175 tag(xml, :Header, header_attributes) { xml << header.to_s } unless header.empty?
  188. 143 tag(xml, :Body, body_attributes) do
  189. 143 if @globals[:no_message_tag]
  190. 1 xml << message.to_s
  191. else
  192. 284 xml.tag!(*namespaced_message_tag) { xml << body_message }
  193. end
  194. end
  195. end
  196. end
  197. 1 def build_multipart_message(message_xml)
  198. 3 multipart_message = init_multipart_message(message_xml)
  199. 3 add_attachments_to_multipart_message(multipart_message)
  200. 3 multipart_message.ready_to_send!
  201. # the mail.body.encoded algorithm reorders the parts, default order is [ "text/plain", "text/enriched", "text/html" ]
  202. # should redefine the sort order, because the soap request xml should be the first
  203. 3 multipart_message.body.set_sort_order [ "text/xml" ]
  204. 3 multipart_message.body.encoded(multipart_message.content_transfer_encoding)
  205. end
  206. 1 def init_multipart_message(message_xml)
  207. 3 multipart_message = Mail.new
  208. 3 xml_part = Mail::Part.new do
  209. 3 content_type 'text/xml'
  210. 3 body message_xml
  211. # in Content-Type the start parameter is recommended (RFC 2387)
  212. 3 content_id '<soap-request-body@soap>'
  213. end
  214. 3 multipart_message.add_part xml_part
  215. #request.headers["Content-Type"] = "multipart/related; boundary=\"#{multipart_message.body.boundary}\"; type=\"text/xml\"; start=\"#{xml_part.content_id}\""
  216. @multipart = {
  217. 3 multipart_boundary: multipart_message.body.boundary,
  218. start: xml_part.content_id,
  219. }
  220. 3 multipart_message
  221. end
  222. 1 def add_attachments_to_multipart_message(multipart_message)
  223. 3 if @locals[:attachments].is_a? Hash
  224. # hash example: { 'att1' => '/path/to/att1', 'att2' => '/path/to/att2' }
  225. 1 @locals[:attachments].each do |identifier, attachment|
  226. 1 add_attachment_to_multipart_message(multipart_message, attachment, identifier)
  227. end
  228. 2 elsif @locals[:attachments].is_a? Array
  229. # array example: [ '/path/to/att1', '/path/to/att2' ]
  230. # array example: [ { filename: 'att1.xml', content: '<x/>' }, { filename: 'att2.xml', content: '<y/>' } ]
  231. 2 @locals[:attachments].each do |attachment|
  232. 4 add_attachment_to_multipart_message(multipart_message, attachment, attachment.is_a?(String) ? File.basename(attachment) : attachment[:filename])
  233. end
  234. end
  235. end
  236. 1 def add_attachment_to_multipart_message(multipart_message, attachment, identifier)
  237. 5 multipart_message.add_file attachment.clone
  238. 5 multipart_message.parts.last.content_id = multipart_message.parts.last.content_location = identifier.to_s
  239. end
  240. end
  241. end

lib/savon/client.rb

100.0% lines covered

47 relevant lines. 47 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "savon/operation"
  3. 1 require "savon/request"
  4. 1 require "savon/options"
  5. 1 require "savon/block_interface"
  6. 1 require "wasabi"
  7. 1 module Savon
  8. 1 class Client
  9. 1 def initialize(globals = {}, &block)
  10. 166 unless globals.kind_of? Hash
  11. 1 raise_version1_initialize_error! globals
  12. end
  13. 165 set_globals(globals, block)
  14. 162 unless wsdl_or_endpoint_and_namespace_specified?
  15. 2 raise_initialization_error!
  16. end
  17. 160 build_wsdl_document
  18. end
  19. 1 attr_reader :globals, :wsdl
  20. 1 def operations
  21. 4 raise_missing_wsdl_error! unless @wsdl.document?
  22. 3 @wsdl.soap_actions
  23. end
  24. 1 def operation(operation_name)
  25. 135 Operation.create(operation_name, @wsdl, @globals)
  26. end
  27. 1 def call(operation_name, locals = {}, &block)
  28. 124 operation(operation_name).call(locals, &block)
  29. end
  30. 1 def service_name
  31. 1 raise_missing_wsdl_error! unless @wsdl.document?
  32. 1 @wsdl.service_name
  33. end
  34. 1 def build_request(operation_name, locals = {}, &block)
  35. 8 operation(operation_name).request(locals, &block)
  36. end
  37. 1 private
  38. 1 def set_globals(globals, block)
  39. 165 globals = GlobalOptions.new(globals)
  40. 163 BlockInterface.new(globals).evaluate(block) if block
  41. 162 @globals = globals
  42. end
  43. 1 def build_wsdl_document
  44. 160 @wsdl = Wasabi::Document.new
  45. 160 @wsdl.document = @globals[:wsdl] if @globals.include? :wsdl
  46. 160 @wsdl.endpoint = @globals[:endpoint] if @globals.include? :endpoint
  47. 160 @wsdl.namespace = @globals[:namespace] if @globals.include? :namespace
  48. 160 @wsdl.adapter = @globals[:adapter] if @globals.include? :adapter
  49. 160 @wsdl.request = WSDLRequest.new(@globals).build
  50. end
  51. 1 def wsdl_or_endpoint_and_namespace_specified?
  52. 162 @globals.include?(:wsdl) || (@globals.include?(:endpoint) && @globals.include?(:namespace))
  53. end
  54. 1 def raise_version1_initialize_error!(object)
  55. 1 raise InitializationError,
  56. "Some code tries to initialize Savon with the #{object.inspect} (#{object.class}) \n" \
  57. "Savon 2 expects a Hash of options for creating a new client and executing requests.\n" \
  58. "Please read the updated documentation for version 2: http://savonrb.com/version2.html"
  59. end
  60. 1 def raise_initialization_error!
  61. 2 raise InitializationError,
  62. "Expected either a WSDL document or the SOAP endpoint and target namespace options.\n\n" \
  63. "Savon.client(wsdl: '/Users/me/project/service.wsdl') # to use a local WSDL document\n" \
  64. "Savon.client(wsdl: 'http://example.com?wsdl') # to use a remote WSDL document\n" \
  65. "Savon.client(endpoint: 'http://example.com', namespace: 'http://v1.example.com') # if you don't have a WSDL document"
  66. end
  67. 1 def raise_missing_wsdl_error!
  68. 1 raise "Unable to inspect the service without a WSDL document."
  69. end
  70. end
  71. end

lib/savon/header.rb

100.0% lines covered

47 relevant lines. 47 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "akami"
  3. 1 require "gyoku"
  4. 1 require "securerandom"
  5. 1 module Savon
  6. 1 class Header
  7. 1 def initialize(globals, locals)
  8. 142 @gyoku_options = { :key_converter => globals[:convert_request_keys_to] }
  9. 142 @wsse_auth = locals[:wsse_auth].nil? ? globals[:wsse_auth] : locals[:wsse_auth]
  10. 142 @wsse_timestamp = locals[:wsse_timestamp].nil? ? globals[:wsse_timestamp] : locals[:wsse_timestamp]
  11. 142 @wsse_signature = locals[:wsse_signature].nil? ? globals[:wsse_signature] : locals[:wsse_signature]
  12. 142 @global_header = globals[:soap_header]
  13. 142 @local_header = locals[:soap_header]
  14. 142 @globals = globals
  15. 142 @locals = locals
  16. 142 @header = build
  17. end
  18. 1 attr_reader :local_header, :global_header, :gyoku_options,
  19. :wsse_auth, :wsse_timestamp, :wsse_signature
  20. 1 def empty?
  21. 143 @header.empty?
  22. end
  23. 1 def to_s
  24. 32 @header
  25. end
  26. 1 private
  27. 1 def build
  28. 142 build_header + build_wsa_header + build_wsse_header
  29. end
  30. 1 def build_header
  31. header =
  32. 142 if global_header.kind_of?(Hash) && local_header.kind_of?(Hash)
  33. 1 global_header.merge(local_header)
  34. 141 elsif local_header
  35. 3 local_header
  36. else
  37. 138 global_header
  38. end
  39. 142 convert_to_xml(header)
  40. end
  41. 1 def build_wsse_header
  42. 142 wsse_header = akami
  43. 142 wsse_header.respond_to?(:to_xml) ? wsse_header.to_xml : ""
  44. end
  45. 1 def build_wsa_header
  46. 142 return '' unless @globals[:use_wsa_headers]
  47. 3 convert_to_xml({
  48. 'wsa:Action' => @locals[:soap_action],
  49. 'wsa:To' => @globals[:endpoint],
  50. 'wsa:MessageID' => "urn:uuid:#{SecureRandom.uuid}"
  51. })
  52. end
  53. 1 def convert_to_xml(hash_or_string)
  54. 145 if hash_or_string.kind_of? Hash
  55. 6 Gyoku.xml(hash_or_string, gyoku_options)
  56. else
  57. 139 hash_or_string.to_s
  58. end
  59. end
  60. 1 def akami
  61. 142 wsse = Akami.wsse
  62. 142 wsse.credentials(*wsse_auth) if wsse_auth
  63. 142 wsse.timestamp = wsse_timestamp if wsse_timestamp
  64. 142 if wsse_signature && wsse_signature.have_document?
  65. 10 wsse.signature = wsse_signature
  66. end
  67. 142 wsse
  68. end
  69. end
  70. end

lib/savon/http_error.rb

100.0% lines covered

12 relevant lines. 12 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module Savon
  3. 1 class HTTPError < Error
  4. 1 def self.present?(http)
  5. 317 http.error?
  6. end
  7. 1 def initialize(http)
  8. 20 @http = http
  9. end
  10. 1 attr_reader :http
  11. 1 def to_s
  12. 5 String.new("HTTP error (#{@http.code})").tap do |str_error|
  13. 5 str_error << ": #{@http.body}" unless @http.body.empty?
  14. end
  15. end
  16. 1 def to_hash
  17. 1 { :code => @http.code, :headers => @http.headers, :body => @http.body }
  18. end
  19. end
  20. end

lib/savon/log_message.rb

100.0% lines covered

29 relevant lines. 29 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "nokogiri"
  3. 1 module Savon
  4. 1 class LogMessage
  5. 1 def initialize(message, filters = [], pretty_print = false)
  6. 12 @message = message
  7. 12 @filters = filters
  8. 12 @pretty_print = pretty_print
  9. end
  10. 1 def to_s
  11. 12 message_is_xml = @message =~ /^</
  12. 12 has_filters = @filters.any?
  13. 12 pretty_print = @pretty_print
  14. 12 return @message unless message_is_xml
  15. 10 return @message unless has_filters || pretty_print
  16. 8 document = Nokogiri.XML(@message)
  17. 8 document = apply_filter(document) if has_filters
  18. 8 document.to_xml(nokogiri_options)
  19. end
  20. 1 private
  21. 1 def apply_filter(document)
  22. 5 return document unless document.errors.empty?
  23. 5 @filters.each do |filter|
  24. 5 apply_filter! document, filter
  25. end
  26. 5 document
  27. end
  28. 1 def apply_filter!(document, filter)
  29. 5 if filter.instance_of? Proc
  30. 1 filter.call document
  31. else
  32. 4 document.xpath("//*[local-name()='#{filter}']").each do |node|
  33. 4 node.content = "***FILTERED***"
  34. end
  35. end
  36. end
  37. 1 def nokogiri_options
  38. 8 @pretty_print ? { :indent => 2 } : { :save_with => Nokogiri::XML::Node::SaveOptions::AS_XML }
  39. end
  40. end
  41. end

lib/savon/message.rb

100.0% lines covered

19 relevant lines. 19 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "savon/qualified_message"
  3. 1 require "gyoku"
  4. 1 module Savon
  5. 1 class Message
  6. 1 def initialize(message_tag, namespace_identifier, types, used_namespaces, message, element_form_default, key_converter, unwrap)
  7. 143 @message_tag = message_tag
  8. 143 @namespace_identifier = namespace_identifier
  9. 143 @types = types
  10. 143 @used_namespaces = used_namespaces
  11. 143 @message = message
  12. 143 @element_form_default = element_form_default
  13. 143 @key_converter = key_converter
  14. 143 @unwrap = unwrap
  15. end
  16. 1 def to_s
  17. 143 return @message.to_s unless @message.kind_of? Hash
  18. 21 if @element_form_default == :qualified
  19. 11 @message = QualifiedMessage.new(@types, @used_namespaces, @key_converter).to_hash(@message, [@message_tag.to_s])
  20. end
  21. gyoku_options = {
  22. 21 :element_form_default => @element_form_default,
  23. :namespace => @namespace_identifier,
  24. :key_converter => @key_converter,
  25. :unwrap => @unwrap
  26. }
  27. 21 Gyoku.xml(@message, gyoku_options)
  28. end
  29. end
  30. end

lib/savon/mock.rb

100.0% lines covered

3 relevant lines. 3 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module Savon
  3. 1 class ExpectationError < StandardError; end
  4. end
  5. 1 require "savon/mock/expectation"

lib/savon/mock/expectation.rb

97.67% lines covered

43 relevant lines. 42 lines covered and 1 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "httpi"
  3. 1 module Savon
  4. 1 class MockExpectation
  5. 1 def initialize(operation_name)
  6. 16 @expected = { :operation_name => operation_name }
  7. 16 @actual = nil
  8. end
  9. 1 def with(locals)
  10. 12 @expected[:message] = locals[:message]
  11. 12 self
  12. end
  13. 1 def returns(response)
  14. 15 response = { :code => 200, :headers => {}, :body => response } if response.kind_of?(String)
  15. 15 @response = response
  16. 15 self
  17. end
  18. 1 def actual(operation_name, builder, globals, locals)
  19. @actual = {
  20. 15 :operation_name => operation_name,
  21. :message => locals[:message]
  22. }
  23. end
  24. 1 def verify!
  25. 15 unless @actual
  26. raise ExpectationError, "Expected a request to the #{@expected[:operation_name].inspect} operation, " \
  27. "but no request was executed."
  28. end
  29. 15 verify_operation_name!
  30. 13 verify_message!
  31. end
  32. 1 def response!
  33. 10 unless @response
  34. 1 raise ExpectationError, "This expectation was not set up with a response."
  35. end
  36. 9 HTTPI::Response.new(@response[:code], @response[:headers], @response[:body])
  37. end
  38. 1 private
  39. 1 def verify_operation_name!
  40. 15 unless @expected[:operation_name] == @actual[:operation_name]
  41. 2 raise ExpectationError, "Expected a request to the #{@expected[:operation_name].inspect} operation.\n" \
  42. "Received a request to the #{@actual[:operation_name].inspect} operation instead."
  43. end
  44. end
  45. 1 def verify_message!
  46. 13 return if @expected[:message].eql? :any
  47. 11 unless equals_except_any(@expected[:message], @actual[:message])
  48. 3 expected_message = " with this message: #{@expected[:message].inspect}" if @expected[:message]
  49. 3 expected_message ||= " with no message."
  50. 3 actual_message = " with this message: #{@actual[:message].inspect}" if @actual[:message]
  51. 3 actual_message ||= " with no message."
  52. 3 raise ExpectationError, "Expected a request to the #{@expected[:operation_name].inspect} operation\n#{expected_message}\n" \
  53. "Received a request to the #{@actual[:operation_name].inspect} operation\n#{actual_message}"
  54. end
  55. end
  56. 1 def equals_except_any(msg_expected, msg_real)
  57. 11 return true if msg_expected === msg_real
  58. 4 return false if (msg_expected.nil? || msg_real.nil?) # If both are nil has returned true
  59. 1 msg_expected.each do |key, expected_value|
  60. 2 next if (expected_value == :any && msg_real.include?(key))
  61. 1 return false if expected_value != msg_real[key]
  62. end
  63. 1 return true
  64. end
  65. end
  66. end

lib/savon/model.rb

100.0% lines covered

33 relevant lines. 33 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module Savon
  3. 1 module Model
  4. 1 def self.extended(base)
  5. 10 base.setup
  6. end
  7. 1 def setup
  8. 10 class_operation_module
  9. 10 instance_operation_module
  10. end
  11. # Accepts one or more SOAP operations and generates both class and instance methods named
  12. # after the given operations. Each generated method accepts an optional SOAP message Hash.
  13. 1 def operations(*operations)
  14. 7 operations.each do |operation|
  15. 19 define_class_operation(operation)
  16. 19 define_instance_operation(operation)
  17. end
  18. end
  19. 1 def all_operations
  20. 1 operations(*client.operations)
  21. end
  22. 1 private
  23. # Defines a class-level SOAP operation.
  24. 1 def define_class_operation(operation)
  25. 19 class_operation_module.module_eval %{
  26. def #{operation.to_s.snakecase}(locals = {})
  27. client.call #{operation.inspect}, locals
  28. end
  29. }
  30. end
  31. # Defines an instance-level SOAP operation.
  32. 1 def define_instance_operation(operation)
  33. 19 instance_operation_module.module_eval %{
  34. def #{operation.to_s.snakecase}(locals = {})
  35. self.class.#{operation.to_s.snakecase} locals
  36. end
  37. }
  38. end
  39. # Class methods.
  40. 1 def class_operation_module
  41. 29 @class_operation_module ||= Module.new {
  42. 10 def client(globals = {})
  43. 28 @client ||= Savon::Client.new(globals)
  44. rescue InitializationError
  45. 1 raise_initialization_error!
  46. end
  47. 10 def global(option, *value)
  48. 5 client.globals[option] = value
  49. end
  50. 10 def raise_initialization_error!
  51. 1 raise InitializationError,
  52. "Expected the model to be initialized with either a WSDL document or the SOAP endpoint and target namespace options.\n" \
  53. "Make sure to setup the model by calling the .client class method before calling the .global method.\n\n" \
  54. "client(wsdl: '/Users/me/project/service.wsdl') # to use a local WSDL document\n" \
  55. "client(wsdl: 'http://example.com?wsdl') # to use a remote WSDL document\n" \
  56. "client(endpoint: 'http://example.com', namespace: 'http://v1.example.com') # if you don't have a WSDL document"
  57. end
  58. 10 }.tap { |mod| extend(mod) }
  59. end
  60. # Instance methods.
  61. 1 def instance_operation_module
  62. 29 @instance_operation_module ||= Module.new {
  63. 10 def client
  64. 1 self.class.client
  65. end
  66. 10 }.tap { |mod| include(mod) }
  67. end
  68. end
  69. end

lib/savon/operation.rb

100.0% lines covered

72 relevant lines. 72 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "savon/options"
  3. 1 require "savon/block_interface"
  4. 1 require "savon/request"
  5. 1 require "savon/builder"
  6. 1 require "savon/response"
  7. 1 require "savon/request_logger"
  8. 1 require "savon/http_error"
  9. 1 require "mail"
  10. 1 module Savon
  11. 1 class Operation
  12. 1 SOAP_REQUEST_TYPE = {
  13. 1 => "text/xml",
  14. 2 => "application/soap+xml"
  15. }
  16. 1 def self.create(operation_name, wsdl, globals)
  17. 158 if wsdl.document?
  18. 124 ensure_name_is_symbol! operation_name
  19. 121 ensure_exists! operation_name, wsdl
  20. end
  21. 152 new(operation_name, wsdl, globals)
  22. end
  23. 1 def self.ensure_exists!(operation_name, wsdl)
  24. 121 unless wsdl.soap_actions.include? operation_name
  25. 2 raise UnknownOperationError, "Unable to find SOAP operation: #{operation_name.inspect}\n" \
  26. "Operations provided by your service: #{wsdl.soap_actions.inspect}"
  27. end
  28. rescue Wasabi::Resolver::HTTPError => e
  29. 1 raise HTTPError.new(e.response)
  30. end
  31. 1 def self.ensure_name_is_symbol!(operation_name)
  32. 124 unless operation_name.kind_of? Symbol
  33. 3 raise ArgumentError, "Expected the first parameter (the name of the operation to call) to be a symbol\n" \
  34. "Actual: #{operation_name.inspect} (#{operation_name.class})"
  35. end
  36. end
  37. 1 def initialize(name, wsdl, globals)
  38. 154 @name = name
  39. 154 @wsdl = wsdl
  40. 154 @globals = globals
  41. 154 @logger = RequestLogger.new(globals)
  42. end
  43. 1 def build(locals = {}, &block)
  44. 148 set_locals(locals, block)
  45. 144 Builder.new(@name, @wsdl, @globals, @locals)
  46. end
  47. 1 def call(locals = {}, &block)
  48. 133 builder = build(locals, &block)
  49. 131 response = Savon.notify_observers(@name, builder, @globals, @locals)
  50. 124 response ||= call_with_logging build_request(builder)
  51. 122 raise_expected_httpi_response! unless response.kind_of?(HTTPI::Response)
  52. 121 create_response(response)
  53. end
  54. 1 def request(locals = {}, &block)
  55. 7 builder = build(locals, &block)
  56. 5 build_request(builder)
  57. end
  58. 1 private
  59. 1 def create_response(response)
  60. 121 Response.new(response, @globals, @locals)
  61. end
  62. 1 def set_locals(locals, block)
  63. 148 locals = LocalOptions.new(locals)
  64. 146 BlockInterface.new(locals).evaluate(block) if block
  65. 144 @locals = locals
  66. end
  67. 1 def call_with_logging(request)
  68. 222 @logger.log(request) { HTTPI.post(request, @globals[:adapter]) }
  69. end
  70. 1 def build_request(builder)
  71. 118 @locals[:soap_action] ||= soap_action
  72. 118 @globals[:endpoint] ||= endpoint
  73. 118 request = SOAPRequest.new(@globals).build(
  74. :soap_action => soap_action,
  75. :cookies => @locals[:cookies],
  76. :headers => @locals[:headers]
  77. )
  78. 118 request.url = endpoint
  79. 118 request.body = builder.to_s
  80. 118 if builder.multipart
  81. 1 request.gzip
  82. 1 request.headers["Content-Type"] = ["multipart/related",
  83. "type=\"#{SOAP_REQUEST_TYPE[@globals[:soap_version]]}\"",
  84. "start=\"#{builder.multipart[:start]}\"",
  85. "boundary=\"#{builder.multipart[:multipart_boundary]}\""].join("; ")
  86. 1 request.headers["MIME-Version"] = "1.0"
  87. end
  88. # TODO: could HTTPI do this automatically in case the header
  89. # was not specified manually? [dh, 2013-01-04]
  90. 118 request.headers["Content-Length"] = request.body.bytesize.to_s
  91. 118 request
  92. end
  93. 1 def soap_action
  94. # soap_action explicitly set to something falsy
  95. 234 return if @locals.include?(:soap_action) && !@locals[:soap_action]
  96. # get the soap_action from local options
  97. 232 @locals[:soap_action] ||
  98. # with no local option, but a wsdl, ask it for the soap_action
  99. @wsdl.document? && @wsdl.soap_action(@name.to_sym) ||
  100. # if there is no soap_action up to this point, fallback to a simple default
  101. Gyoku.xml_tag(@name, :key_converter => @globals[:convert_request_keys_to])
  102. end
  103. 1 def endpoint
  104. 120 @globals[:endpoint] || @wsdl.endpoint.tap do |url|
  105. 2 if @globals[:host]
  106. 1 host_url = URI.parse(@globals[:host])
  107. 1 url.host = host_url.host
  108. 1 url.port = host_url.port
  109. end
  110. end
  111. end
  112. 1 def raise_expected_httpi_response!
  113. 1 raise Error, "Observers need to return an HTTPI::Response to mock " \
  114. "the request or nil to execute the request."
  115. end
  116. end
  117. end

lib/savon/options.rb

100.0% lines covered

177 relevant lines. 177 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "logger"
  3. 1 require "httpi"
  4. 1 module Savon
  5. 1 class Options
  6. 1 def initialize(options = {})
  7. 541 @options = {}
  8. 541 assign options
  9. end
  10. 1 attr_reader :option_type
  11. 1 def [](option)
  12. 6815 @options[option]
  13. end
  14. 1 def []=(option, value)
  15. 152 value = [value].flatten
  16. 152 self.send(option, *value)
  17. end
  18. 1 def include?(option)
  19. 9899 @options.key? option
  20. end
  21. 1 private
  22. 1 def assign(options)
  23. 541 options.each do |option, value|
  24. 7313 self.send(option, value)
  25. end
  26. end
  27. 1 def method_missing(option, _)
  28. 6 raise UnknownOptionError, "Unknown #{option_type} option: #{option.inspect}"
  29. end
  30. end
  31. 1 module SharedOptions
  32. # WSSE auth credentials for Akami.
  33. # Local will override the global wsse_auth value, e.g.
  34. # global == [user, pass] && local == [user2, pass2] => [user2, pass2]
  35. # global == [user, pass] && local == false => false
  36. # global == [user, pass] && local == nil => [user, pass]
  37. 1 def wsse_auth(*credentials)
  38. 15 credentials.flatten!
  39. 15 if credentials.size == 1
  40. 4 @options[:wsse_auth] = credentials.first
  41. else
  42. 11 @options[:wsse_auth] = credentials
  43. end
  44. end
  45. # Instruct Akami to enable wsu:Timestamp headers.
  46. # Local will override the global wsse_timestamp value, e.g.
  47. # global == true && local == true => true
  48. # global == true && local == false => false
  49. # global == true && local == nil => true
  50. 1 def wsse_timestamp(timestamp = true)
  51. 11 @options[:wsse_timestamp] = timestamp
  52. end
  53. 1 def wsse_signature(signature)
  54. 4 @options[:wsse_signature] = signature
  55. end
  56. end
  57. 1 class GlobalOptions < Options
  58. 1 include SharedOptions
  59. 1 def initialize(options = {})
  60. 330 @option_type = :global
  61. defaults = {
  62. 330 :encoding => "UTF-8",
  63. :soap_version => 1,
  64. :namespaces => {},
  65. :logger => Logger.new($stdout),
  66. :log => false,
  67. :filters => [],
  68. :pretty_print_xml => false,
  69. :raise_errors => true,
  70. :strip_namespaces => true,
  71. :delete_namespace_attributes => false,
  72. 467 :convert_response_tags_to => lambda { |tag| tag.snakecase.to_sym},
  73. 195 :convert_attributes_to => lambda { |k,v| [k,v] },
  74. :multipart => false,
  75. :adapter => nil,
  76. :use_wsa_headers => false,
  77. :no_message_tag => false,
  78. :follow_redirects => false,
  79. :unwrap => false,
  80. :host => nil
  81. }
  82. 330 options = defaults.merge(options)
  83. # this option is a shortcut on the logger which needs to be set
  84. # before it can be modified to set the option.
  85. 330 delayed_level = options.delete(:log_level)
  86. 330 super(options)
  87. 329 log_level(delayed_level) unless delayed_level.nil?
  88. end
  89. # Location of the local or remote WSDL document.
  90. 1 def wsdl(wsdl_address)
  91. 128 @options[:wsdl] = wsdl_address
  92. end
  93. # set different host for actions in WSDL
  94. 1 def host(host)
  95. 330 @options[:host] = host
  96. end
  97. # SOAP endpoint.
  98. 1 def endpoint(endpoint)
  99. 156 @options[:endpoint] = endpoint
  100. end
  101. # Target namespace.
  102. 1 def namespace(namespace)
  103. 38 @options[:namespace] = namespace
  104. end
  105. # The namespace identifer.
  106. 1 def namespace_identifier(identifier)
  107. 3 @options[:namespace_identifier] = identifier
  108. end
  109. # Namespaces for the SOAP envelope.
  110. 1 def namespaces(namespaces)
  111. 330 @options[:namespaces] = namespaces
  112. end
  113. # Proxy server to use for all requests.
  114. 1 def proxy(proxy)
  115. 3 @options[:proxy] = proxy unless proxy.nil?
  116. end
  117. # A Hash of HTTP headers.
  118. 1 def headers(headers)
  119. 5 @options[:headers] = headers
  120. end
  121. # Open timeout in seconds.
  122. 1 def open_timeout(open_timeout)
  123. 5 @options[:open_timeout] = open_timeout
  124. end
  125. # Read timeout in seconds.
  126. 1 def read_timeout(read_timeout)
  127. 3 @options[:read_timeout] = read_timeout
  128. end
  129. # Write timeout in seconds.
  130. 1 def write_timeout(write_timeout)
  131. 1 @options[:write_timeout] = write_timeout
  132. end
  133. # The encoding to use. Defaults to "UTF-8".
  134. 1 def encoding(encoding)
  135. 331 @options[:encoding] = encoding
  136. end
  137. # The global SOAP header. Expected to be a Hash or responding to #to_s.
  138. 1 def soap_header(header)
  139. 4 @options[:soap_header] = header
  140. end
  141. # Sets whether elements should be :qualified or :unqualified.
  142. # If you need to use this option, please open an issue and make
  143. # sure to add your WSDL document for debugging.
  144. 1 def element_form_default(element_form_default)
  145. 7 @options[:element_form_default] = element_form_default
  146. end
  147. # Can be used to change the SOAP envelope namespace identifier.
  148. # If you need to use this option, please open an issue and make
  149. # sure to add your WSDL document for debugging.
  150. 1 def env_namespace(env_namespace)
  151. 2 @options[:env_namespace] = env_namespace
  152. end
  153. # Changes the SOAP version to 1 or 2.
  154. 1 def soap_version(soap_version)
  155. 332 @options[:soap_version] = soap_version
  156. end
  157. # Whether or not to raise SOAP fault and HTTP errors.
  158. 1 def raise_errors(raise_errors)
  159. 343 @options[:raise_errors] = raise_errors
  160. end
  161. # Whether or not to log.
  162. 1 def log(log)
  163. 334 HTTPI.log = log
  164. 334 @options[:log] = log
  165. end
  166. # The logger to use. Defaults to a Savon::Logger instance.
  167. 1 def logger(logger)
  168. 330 HTTPI.logger = logger
  169. 330 @options[:logger] = logger
  170. end
  171. # Changes the Logger's log level.
  172. 1 def log_level(level)
  173. 6 levels = { :debug => 0, :info => 1, :warn => 2, :error => 3, :fatal => 4 }
  174. 6 unless levels.include? level
  175. 1 raise ArgumentError, "Invalid log level: #{level.inspect}\n" \
  176. "Expected one of: #{levels.keys.inspect}"
  177. end
  178. 5 @options[:logger].level = levels[level]
  179. end
  180. # A list of XML tags to filter from logged SOAP messages.
  181. 1 def filters(*filters)
  182. 330 @options[:filters] = filters.flatten
  183. end
  184. # Whether to pretty print request and response XML log messages.
  185. 1 def pretty_print_xml(pretty_print_xml)
  186. 330 @options[:pretty_print_xml] = pretty_print_xml
  187. end
  188. # Specifies the SSL version to use.
  189. 1 def ssl_version(version)
  190. 3 @options[:ssl_version] = version
  191. end
  192. # Specifies the SSL version to use.
  193. 1 def ssl_min_version(version)
  194. 2 @options[:ssl_min_version] = version
  195. end
  196. # Specifies the SSL version to use.
  197. 1 def ssl_max_version(version)
  198. 2 @options[:ssl_max_version] = version
  199. end
  200. # Whether and how to to verify the connection.
  201. 1 def ssl_verify_mode(verify_mode)
  202. 3 @options[:ssl_verify_mode] = verify_mode
  203. end
  204. # Sets the cert key file to use.
  205. 1 def ssl_cert_key_file(file)
  206. 6 @options[:ssl_cert_key_file] = file
  207. end
  208. # Sets the cert key to use.
  209. 1 def ssl_cert_key(key)
  210. 1 @options[:ssl_cert_key] = key
  211. end
  212. # Sets the cert key password to use.
  213. 1 def ssl_cert_key_password(password)
  214. 5 @options[:ssl_cert_key_password] = password
  215. end
  216. # Sets the cert file to use.
  217. 1 def ssl_cert_file(file)
  218. 5 @options[:ssl_cert_file] = file
  219. end
  220. # Sets the cert to use.
  221. 1 def ssl_cert(cert)
  222. 1 @options[:ssl_cert] = cert
  223. end
  224. # Sets the ca cert file to use.
  225. 1 def ssl_ca_cert_file(file)
  226. 3 @options[:ssl_ca_cert_file] = file
  227. end
  228. # Sets the ca cert to use.
  229. 1 def ssl_ca_cert(cert)
  230. 1 @options[:ssl_ca_cert] = cert
  231. end
  232. 1 def ssl_ciphers(ciphers)
  233. 3 @options[:ssl_ciphers] = ciphers
  234. end
  235. # Sets the ca cert path.
  236. 1 def ssl_ca_cert_path(path)
  237. 1 @options[:ssl_ca_cert_path] = path
  238. end
  239. # Sets the ssl cert store.
  240. 1 def ssl_cert_store(store)
  241. 1 @options[:ssl_cert_store] = store
  242. end
  243. # HTTP basic auth credentials.
  244. 1 def basic_auth(*credentials)
  245. 3 @options[:basic_auth] = credentials.flatten
  246. end
  247. # HTTP digest auth credentials.
  248. 1 def digest_auth(*credentials)
  249. 3 @options[:digest_auth] = credentials.flatten
  250. end
  251. # NTLM auth credentials.
  252. 1 def ntlm(*credentials)
  253. 3 @options[:ntlm] = credentials.flatten
  254. end
  255. # Instruct Nori whether to strip namespaces from XML nodes.
  256. 1 def strip_namespaces(strip_namespaces)
  257. 333 @options[:strip_namespaces] = strip_namespaces
  258. end
  259. # Instruct Nori whether to delete namespace attributes from XML nodes.
  260. 1 def delete_namespace_attributes(delete_namespace_attributes)
  261. 330 @options[:delete_namespace_attributes] = delete_namespace_attributes
  262. end
  263. # Tell Gyoku how to convert Hash key Symbols to XML tags.
  264. # Accepts one of :lower_camelcase, :camelcase, :upcase, or :none.
  265. 1 def convert_request_keys_to(converter)
  266. 5 @options[:convert_request_keys_to] = converter
  267. end
  268. # Tell Gyoku to unwrap Array of Hashes
  269. # Accepts a boolean, default to false
  270. 1 def unwrap(unwrap)
  271. 330 @options[:unwrap] = unwrap
  272. end
  273. # Tell Nori how to convert XML tags from the SOAP response into Hash keys.
  274. # Accepts a lambda or a block which receives an XML tag and returns a Hash key.
  275. # Defaults to convert tags to snakecase Symbols.
  276. 1 def convert_response_tags_to(converter = nil, &block)
  277. 334 @options[:convert_response_tags_to] = block || converter
  278. end
  279. # Tell Nori how to convert XML attributes on tags from the SOAP response into Hash keys.
  280. # Accepts a lambda or a block which receives an XML tag and returns a Hash key.
  281. # Defaults to doing nothing
  282. 1 def convert_attributes_to(converter = nil, &block)
  283. 332 @options[:convert_attributes_to] = block || converter
  284. end
  285. # Instruct Savon to create a multipart response if available.
  286. 1 def multipart(multipart)
  287. 330 @options[:multipart] = multipart
  288. end
  289. # Instruct Savon what HTTPI adapter it should use instead of default
  290. 1 def adapter(adapter)
  291. 330 @options[:adapter] = adapter
  292. end
  293. # Enable inclusion of WS-Addressing headers.
  294. 1 def use_wsa_headers(use)
  295. 330 @options[:use_wsa_headers] = use
  296. end
  297. 1 def no_message_tag(bool)
  298. 330 @options[:no_message_tag] = bool
  299. end
  300. # Instruct requests to follow HTTP redirects.
  301. 1 def follow_redirects(follow_redirects)
  302. 330 @options[:follow_redirects] = follow_redirects
  303. end
  304. end
  305. 1 class LocalOptions < Options
  306. 1 include SharedOptions
  307. 1 def initialize(options = {})
  308. 211 @option_type = :local
  309. 211 defaults = {
  310. :advanced_typecasting => true,
  311. :response_parser => :nokogiri,
  312. :multipart => false
  313. }
  314. 211 super defaults.merge(options)
  315. end
  316. # The local SOAP header. Expected to be a Hash or respond to #to_s.
  317. # Will be merged with the global SOAP header if both are Hashes.
  318. # Otherwise the local option will be prefered.
  319. 1 def soap_header(header)
  320. 4 @options[:soap_header] = header
  321. end
  322. # The SOAP message to send. Expected to be a Hash or a String.
  323. 1 def message(message)
  324. 31 @options[:message] = message
  325. end
  326. # SOAP message tag (formerly known as SOAP input tag). If it's not set, Savon retrieves the name from
  327. # the WSDL document (if available). Otherwise, Gyoku converts the operation name into an XML element.
  328. 1 def message_tag(message_tag)
  329. 2 @options[:message_tag] = message_tag
  330. end
  331. # Attributes for the SOAP message tag.
  332. 1 def attributes(attributes)
  333. 3 @options[:attributes] = attributes
  334. end
  335. # Attachments for the SOAP message (https://www.w3.org/TR/SOAP-attachments)
  336. #
  337. # should pass an Array or a Hash; items should be path strings or
  338. # { filename: 'file.name', content: 'content' } objects
  339. # The Content-ID in multipart message sections will be the filename or the key if Hash is given
  340. #
  341. # usage examples:
  342. #
  343. # response = client.call :operation1 do
  344. # message param1: 'value'
  345. # attachments [
  346. # { filename: 'x1.xml', content: '<xml>abc</xml>'},
  347. # { filename: 'x2.xml', content: '<xml>abc</xml>'}
  348. # ]
  349. # end
  350. # # Content-ID will be x1.xml and x2.xml
  351. #
  352. # response = client.call :operation1 do
  353. # message param1: 'value'
  354. # attachments 'x1.xml' => '/tmp/1281ab7d7d.xml', 'x2.xml' => '/tmp/4c5v8e833a.xml'
  355. # end
  356. # # Content-ID will be x1.xml and x2.xml
  357. #
  358. # response = client.call :operation1 do
  359. # message param1: 'value'
  360. # attachments [ '/tmp/1281ab7d7d.xml', '/tmp/4c5v8e833a.xml']
  361. # end
  362. # # Content-ID will be 1281ab7d7d.xml and 4c5v8e833a.xml
  363. #
  364. # The Content-ID is important if you want to refer to the attachments from the SOAP request
  365. 1 def attachments(attachments)
  366. 3 @options[:attachments] = attachments
  367. end
  368. # Value of the SOAPAction HTTP header.
  369. 1 def soap_action(soap_action)
  370. 119 @options[:soap_action] = soap_action
  371. end
  372. # Cookies to be used for the next request.
  373. 1 def cookies(cookies)
  374. 2 @options[:cookies] = cookies
  375. end
  376. # The SOAP request XML to send. Expected to be a String.
  377. 1 def xml(xml)
  378. 14 @options[:xml] = xml
  379. end
  380. # Instruct Nori to use advanced typecasting.
  381. 1 def advanced_typecasting(advanced)
  382. 211 @options[:advanced_typecasting] = advanced
  383. end
  384. # Instruct Nori to use :rexml or :nokogiri to parse the response.
  385. 1 def response_parser(parser)
  386. 211 @options[:response_parser] = parser
  387. end
  388. # Instruct Savon to create a multipart response if available.
  389. 1 def multipart(multipart)
  390. 211 @options[:multipart] = multipart
  391. end
  392. 1 def headers(headers)
  393. 1 @options[:headers] = headers
  394. end
  395. end
  396. end

lib/savon/qualified_message.rb

100.0% lines covered

31 relevant lines. 31 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "gyoku"
  3. 1 module Savon
  4. 1 class QualifiedMessage
  5. 1 def initialize(types, used_namespaces, key_converter)
  6. 14 @types = types
  7. 14 @used_namespaces = used_namespaces
  8. 14 @key_converter = key_converter
  9. end
  10. 1 def to_hash(hash, path)
  11. 65 return hash unless hash
  12. 65 return hash.map { |value| to_hash(value, path) } if hash.is_a?(Array)
  13. 61 return hash.to_s unless hash.is_a?(Hash)
  14. 28 hash.each_with_object({}) do |(key, value), newhash|
  15. 52 case key
  16. when :order!
  17. 2 newhash[key] = add_namespaces_to_values(value, path)
  18. when :attributes!, :content!
  19. 3 newhash[key] = to_hash(value, path)
  20. else
  21. 47 if key.to_s =~ /!$/
  22. 1 newhash[key] = value
  23. else
  24. 46 translated_key = translate_tag(key)
  25. 46 newkey = add_namespaces_to_values(key, path).first
  26. 46 newpath = path + [translated_key]
  27. 46 newhash[newkey] = to_hash(value, @types[newpath] ? [@types[newpath]] : newpath)
  28. end
  29. end
  30. 52 newhash
  31. end
  32. end
  33. 1 private
  34. 1 def translate_tag(key)
  35. 97 Gyoku.xml_tag(key, :key_converter => @key_converter).to_s
  36. end
  37. 1 def add_namespaces_to_values(values, path)
  38. 48 Array(values).collect do |value|
  39. 51 translated_value = translate_tag(value)
  40. 51 namespace_path = path + [translated_value]
  41. 51 namespace = @used_namespaces[namespace_path] || ''
  42. 51 namespace.empty? ? value : "#{namespace}:#{translated_value}"
  43. end
  44. end
  45. end
  46. end

lib/savon/request.rb

98.53% lines covered

68 relevant lines. 67 lines covered and 1 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "httpi"
  3. 1 module Savon
  4. 1 class HTTPRequest
  5. 1 def initialize(globals, http_request = nil)
  6. 351 @globals = globals
  7. 351 @http_request = http_request || HTTPI::Request.new
  8. end
  9. 1 def build
  10. @http_request
  11. end
  12. 1 private
  13. 1 def configure_proxy
  14. 350 @http_request.proxy = @globals[:proxy] if @globals.include? :proxy
  15. end
  16. 1 def configure_timeouts
  17. 350 @http_request.open_timeout = @globals[:open_timeout] if @globals.include? :open_timeout
  18. 350 @http_request.read_timeout = @globals[:read_timeout] if @globals.include? :read_timeout
  19. 350 @http_request.write_timeout = @globals[:write_timeout] if @globals.include? :write_timeout
  20. end
  21. 1 def configure_ssl
  22. 350 @http_request.auth.ssl.ssl_version = @globals[:ssl_version] if @globals.include? :ssl_version
  23. 350 @http_request.auth.ssl.min_version = @globals[:ssl_min_version] if @globals.include? :ssl_min_version
  24. 350 @http_request.auth.ssl.max_version = @globals[:ssl_max_version] if @globals.include? :ssl_max_version
  25. 350 @http_request.auth.ssl.verify_mode = @globals[:ssl_verify_mode] if @globals.include? :ssl_verify_mode
  26. 350 @http_request.auth.ssl.ciphers = @globals[:ssl_ciphers] if @globals.include? :ssl_ciphers
  27. 350 @http_request.auth.ssl.cert_key_file = @globals[:ssl_cert_key_file] if @globals.include? :ssl_cert_key_file
  28. 350 @http_request.auth.ssl.cert_key = @globals[:ssl_cert_key] if @globals.include? :ssl_cert_key
  29. 350 @http_request.auth.ssl.cert_file = @globals[:ssl_cert_file] if @globals.include? :ssl_cert_file
  30. 350 @http_request.auth.ssl.cert = @globals[:ssl_cert] if @globals.include? :ssl_cert
  31. 350 @http_request.auth.ssl.ca_cert_file = @globals[:ssl_ca_cert_file] if @globals.include? :ssl_ca_cert_file
  32. 350 @http_request.auth.ssl.ca_cert_path = @globals[:ssl_ca_cert_path] if @globals.include? :ssl_ca_cert_path
  33. 350 @http_request.auth.ssl.ca_cert = @globals[:ssl_ca_cert] if @globals.include? :ssl_ca_cert
  34. 350 @http_request.auth.ssl.cert_store = @globals[:ssl_cert_store] if @globals.include? :ssl_cert_store
  35. 350 @http_request.auth.ssl.cert_key_password = @globals[:ssl_cert_key_password] if @globals.include? :ssl_cert_key_password
  36. end
  37. 1 def configure_auth
  38. 350 @http_request.auth.basic(*@globals[:basic_auth]) if @globals.include? :basic_auth
  39. 350 @http_request.auth.digest(*@globals[:digest_auth]) if @globals.include? :digest_auth
  40. 350 @http_request.auth.ntlm(*@globals[:ntlm]) if @globals.include? :ntlm
  41. end
  42. 1 def configure_redirect_handling
  43. 350 if @globals.include? :follow_redirects
  44. 350 @http_request.follow_redirect = @globals[:follow_redirects]
  45. end
  46. end
  47. end
  48. 1 class WSDLRequest < HTTPRequest
  49. 1 def build
  50. 196 configure_proxy
  51. 196 configure_timeouts
  52. 196 configure_headers
  53. 196 configure_ssl
  54. 196 configure_auth
  55. 196 configure_redirect_handling
  56. 196 @http_request
  57. end
  58. 1 private
  59. 1 def configure_headers
  60. 196 @http_request.headers = @globals[:headers] if @globals.include? :headers
  61. end
  62. end
  63. 1 class SOAPRequest < HTTPRequest
  64. 1 CONTENT_TYPE = {
  65. 1 => "text/xml;charset=%s",
  66. 2 => "application/soap+xml;charset=%s"
  67. }
  68. 1 def build(options = {})
  69. 154 configure_proxy
  70. 154 configure_timeouts
  71. 154 configure_headers options[:soap_action], options[:headers]
  72. 154 configure_cookies options[:cookies]
  73. 154 configure_ssl
  74. 154 configure_auth
  75. 154 configure_redirect_handling
  76. 154 @http_request
  77. end
  78. 1 private
  79. 1 def configure_cookies(cookies)
  80. 154 @http_request.set_cookies(cookies) if cookies
  81. end
  82. 1 def configure_headers(soap_action, headers)
  83. 154 @http_request.headers = @globals[:headers] if @globals.include? :headers
  84. 154 @http_request.headers.merge!(headers) if headers
  85. 154 @http_request.headers["SOAPAction"] ||= %{"#{soap_action}"} if soap_action
  86. 154 @http_request.headers["Content-Type"] ||= CONTENT_TYPE[@globals[:soap_version]] % @globals[:encoding]
  87. end
  88. end
  89. end

lib/savon/request_logger.rb

100.0% lines covered

27 relevant lines. 27 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "savon/log_message"
  3. 1 module Savon
  4. 1 class RequestLogger
  5. 1 def initialize(globals)
  6. 155 @globals = globals
  7. end
  8. 1 def log(request, &http_request)
  9. 112 log_request(request) if log?
  10. 112 response = http_request.call
  11. 110 log_response(response) if log?
  12. 110 response
  13. end
  14. 1 def logger
  15. 24 @globals[:logger]
  16. end
  17. 1 def log?
  18. 222 @globals[:log]
  19. end
  20. 1 private
  21. 1 def log_request(request)
  22. 7 logger.info { "SOAP request: #{request.url}" }
  23. 7 logger.info { headers_to_log(request.headers) }
  24. 7 logger.debug { body_to_log(request.body) }
  25. end
  26. 1 def log_response(response)
  27. 7 logger.info { "SOAP response (status #{response.code})" }
  28. 7 logger.debug { headers_to_log(response.headers) }
  29. 7 logger.debug { body_to_log(response.body) }
  30. end
  31. 1 def headers_to_log(headers)
  32. 21 headers.map { |key, value| "#{key}: #{value}" }.join("\n")
  33. end
  34. 1 def body_to_log(body)
  35. 6 LogMessage.new(body, @globals[:filters], @globals[:pretty_print_xml]).to_s
  36. end
  37. end
  38. end

lib/savon/response.rb

100.0% lines covered

88 relevant lines. 88 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "nori"
  3. 1 require "savon/soap_fault"
  4. 1 require "savon/http_error"
  5. 1 module Savon
  6. 1 class Response
  7. 1 CRLF = /\r\n/
  8. 1 WSP = /[#{%Q|\x9\x20|}]/
  9. 1 def initialize(http, globals, locals)
  10. 164 @http = http
  11. 164 @globals = globals
  12. 164 @locals = locals
  13. 164 @attachments = []
  14. 164 @xml = ''
  15. 164 @has_parsed_body = false
  16. 164 build_soap_and_http_errors!
  17. 164 raise_soap_and_http_errors! if @globals[:raise_errors]
  18. end
  19. 1 attr_reader :http, :globals, :locals, :soap_fault, :http_error
  20. 1 def success?
  21. 6 !soap_fault? && !http_error?
  22. end
  23. 1 alias_method :successful?, :success?
  24. 1 def soap_fault?
  25. 322 SOAPFault.present?(@http, xml)
  26. end
  27. 1 def http_error?
  28. 315 HTTPError.present? @http
  29. end
  30. 1 def header
  31. 6 find('Header')
  32. end
  33. 1 def body
  34. 22 find('Body')
  35. end
  36. 1 alias_method :to_hash, :body
  37. 1 def to_array(*path)
  38. 4 result = path.inject body do |memo, key|
  39. 8 return [] if memo[key].nil?
  40. 6 memo[key]
  41. end
  42. 2 result.kind_of?(Array) ? result.compact : [result].compact
  43. end
  44. 1 def hash
  45. 34 @hash ||= nori.parse(xml)
  46. end
  47. 1 def xml
  48. 374 if multipart?
  49. 3 parse_body unless @has_parsed_body
  50. 3 @xml
  51. else
  52. 371 @http.body
  53. end
  54. end
  55. 1 alias_method :to_xml, :xml
  56. 1 alias_method :to_s, :xml
  57. 1 def doc
  58. 5 @doc ||= Nokogiri.XML(xml)
  59. end
  60. 1 def xpath(path, namespaces = nil)
  61. 2 doc.xpath(path, namespaces || xml_namespaces)
  62. end
  63. 1 def find(*path)
  64. 30 envelope = nori.find(hash, 'Envelope')
  65. 30 raise_invalid_response_error! unless envelope.is_a?(Hash)
  66. 28 nori.find(envelope, *path)
  67. end
  68. 1 def attachments
  69. 2 if multipart?
  70. 1 parse_body unless @has_parsed_body
  71. 1 @attachments
  72. else
  73. 1 []
  74. end
  75. end
  76. 1 def multipart?
  77. 379 !(http.headers['content-type'] =~ /^multipart/im).nil?
  78. end
  79. 1 private
  80. 1 def boundary
  81. 1 return unless multipart?
  82. 1 Mail::Field.new('content-type', http.headers['content-type']).parameters['boundary']
  83. end
  84. 1 def parse_body
  85. 1 http.body.force_encoding Encoding::ASCII_8BIT
  86. 1 parts = http.body.split(/(?:\A|\r\n)--#{Regexp.escape(boundary)}(?=(?:--)?\s*$)/)
  87. 1 parts[1..-1].to_a.each_with_index do |part, index|
  88. 3 header_part, body_part = part.lstrip.split(/#{CRLF}#{CRLF}|#{CRLF}#{WSP}*#{CRLF}(?!#{WSP})/m, 2)
  89. 3 section = Mail::Part.new(
  90. body: body_part
  91. )
  92. 3 section.header = header_part
  93. 3 if index == 0
  94. 1 @xml = section.body.to_s
  95. else
  96. 2 @attachments << section
  97. end
  98. end
  99. 1 @has_parsed_body = true
  100. end
  101. 1 def build_soap_and_http_errors!
  102. 164 @soap_fault = SOAPFault.new(@http, nori, xml) if soap_fault?
  103. 164 @http_error = HTTPError.new(@http) if http_error?
  104. end
  105. 1 def raise_soap_and_http_errors!
  106. 148 raise soap_fault if soap_fault?
  107. 145 raise http_error if http_error?
  108. end
  109. 1 def raise_invalid_response_error!
  110. 2 raise InvalidResponseError, "Unable to parse response body:\n" + xml.inspect
  111. end
  112. 1 def xml_namespaces
  113. 2 @xml_namespaces ||= doc.collect_namespaces
  114. end
  115. 1 def nori
  116. 100 return @nori if @nori
  117. nori_options = {
  118. 42 :delete_namespace_attributes => @globals[:delete_namespace_attributes],
  119. :strip_namespaces => @globals[:strip_namespaces],
  120. :convert_tags_to => @globals[:convert_response_tags_to],
  121. :convert_attributes_to => @globals[:convert_attributes_to],
  122. :advanced_typecasting => @locals[:advanced_typecasting],
  123. :parser => @locals[:response_parser]
  124. }
  125. 294 non_nil_nori_options = nori_options.reject { |_, value| value.nil? }
  126. 42 @nori = Nori.new(non_nil_nori_options)
  127. end
  128. end
  129. end

lib/savon/soap_fault.rb

100.0% lines covered

29 relevant lines. 29 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module Savon
  3. 1 class SOAPFault < Error
  4. 1 def self.present?(http, xml = nil)
  5. 327 xml ||= http.body
  6. 327 fault_node = xml.include?("Fault>")
  7. 327 soap1_fault = xml.match(/faultcode\/?\>/) && xml.match(/faultstring\/?\>/)
  8. 327 soap2_fault = xml.include?("Code>") && xml.include?("Reason>")
  9. 327 fault_node && (soap1_fault || soap2_fault)
  10. end
  11. 1 def initialize(http, nori, xml = nil)
  12. 29 @xml = xml
  13. 29 @http = http
  14. 29 @nori = nori
  15. end
  16. 1 attr_reader :http, :nori, :xml
  17. 1 def to_s
  18. 14 fault = nori.find(to_hash, 'Fault') || nori.find(to_hash, 'ServiceFault')
  19. 14 message_by_version(fault)
  20. end
  21. 1 def to_hash
  22. 22 parsed = nori.parse(xml || http.body)
  23. 22 nori.find(parsed, 'Envelope', 'Body') || {}
  24. end
  25. 1 private
  26. 1 def message_by_version(fault)
  27. 14 if nori.find(fault, 'faultcode')
  28. 8 code = nori.find(fault, 'faultcode')
  29. 8 text = nori.find(fault, 'faultstring')
  30. 8 "(#{code}) #{text}"
  31. 6 elsif nori.find(fault, 'Code')
  32. 4 code = nori.find(fault, 'Code', 'Value')
  33. 4 text = nori.find(fault, 'Reason', 'Text')
  34. 4 "(#{code}) #{text}"
  35. end
  36. end
  37. end
  38. end