soaspec 0.2.8 → 0.2.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/.gitlab-ci.yml +0 -17
  3. data/ChangeLog +6 -0
  4. data/Gemfile +2 -2
  5. data/README.md +42 -9
  6. data/Rakefile +1 -1
  7. data/exe/soaspec +9 -14
  8. data/exe/xml_to_yaml_file +3 -3
  9. data/images/basic_demo.gif +0 -0
  10. data/lib/soaspec.rb +9 -19
  11. data/lib/soaspec/core_ext/hash.rb +10 -12
  12. data/lib/soaspec/cucumber/generic_steps.rb +1 -1
  13. data/lib/soaspec/exchange.rb +4 -1
  14. data/lib/soaspec/exchange_handlers/exchange_handler.rb +5 -4
  15. data/lib/soaspec/exchange_handlers/handler_accessors.rb +7 -3
  16. data/lib/soaspec/exchange_handlers/response_extractor.rb +55 -0
  17. data/lib/soaspec/exchange_handlers/rest_exchanger_factory.rb +1 -1
  18. data/lib/soaspec/exchange_handlers/rest_handler.rb +16 -43
  19. data/lib/soaspec/exchange_handlers/rest_methods.rb +1 -2
  20. data/lib/soaspec/exchange_handlers/rest_parameters.rb +6 -2
  21. data/lib/soaspec/exchange_handlers/rest_parameters_defaults.rb +1 -1
  22. data/lib/soaspec/exchange_handlers/soap_handler.rb +6 -10
  23. data/lib/soaspec/exchange_properties.rb +1 -2
  24. data/lib/soaspec/exe_helpers.rb +7 -9
  25. data/lib/soaspec/indifferent_hash.rb +1 -1
  26. data/lib/soaspec/interpreter.rb +1 -3
  27. data/lib/soaspec/matchers.rb +1 -2
  28. data/lib/soaspec/o_auth2.rb +2 -1
  29. data/lib/soaspec/spec_logger.rb +54 -12
  30. data/lib/soaspec/template_reader.rb +2 -1
  31. data/lib/soaspec/test_server/get_bank.rb +5 -5
  32. data/lib/soaspec/test_server/id_manager.rb +1 -3
  33. data/lib/soaspec/test_server/invoices.rb +0 -1
  34. data/lib/soaspec/test_server/puppy_service.rb +0 -1
  35. data/lib/soaspec/test_server/test_attribute.rb +0 -1
  36. data/lib/soaspec/version.rb +2 -2
  37. data/lib/soaspec/virtual_server.rb +1 -1
  38. data/lib/soaspec/wait.rb +1 -1
  39. data/lib/soaspec/wsdl_generator.rb +11 -3
  40. data/soaspec.gemspec +7 -2
  41. data/test_wsdl.rb +4 -7
  42. metadata +54 -10
@@ -0,0 +1,55 @@
1
+ module Soaspec
2
+ # Enables extracting a response according to type / path
3
+ module ResponseExtractor
4
+ # Convert XML or JSON response into a Hash. Doesn't accept empty body
5
+ # @param [String] response Response as a String (either in XML or JSON)
6
+ # @return [Hash] Extracted Hash from response
7
+ def extract_hash(response)
8
+ raise NoElementAtPath, "Empty Body. Can't assert on it" if response.body.empty?
9
+
10
+ case Interpreter.response_type_for response
11
+ when :xml then parse_xml(response.body.to_s)
12
+ when :json
13
+ converted = JSON.parse(response.body)
14
+ return converted.transform_keys_to_symbols if converted.is_a? Hash
15
+ return converted.map!(&:transform_keys_to_symbols) if converted.is_a? Array
16
+
17
+ raise 'Incorrect Type produced ' + converted.class
18
+ else
19
+ raise "Neither XML nor JSON detected. It is #{type}. Don't know how to parse It is #{response.body}"
20
+ end
21
+ end
22
+
23
+ # @return [Hash] Hash representing response body
24
+ def to_hash(response)
25
+ case Interpreter.response_type_for(response)
26
+ when :xml then parse_xml(response.body.to_s)
27
+ when :json
28
+ JSON.parse(response.body.to_s)
29
+ else
30
+ raise "Unable to interpret type of #{response.body}"
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ # @param [String] xml XML to convert
37
+ # @return [Hash] Hash representing XML
38
+ def parse_xml(xml)
39
+ parser = Nori.new(strip_namespaces: strip_namespaces?, convert_tags_to: ->(tag) { tag.snakecase.to_sym })
40
+ parser.parse(xml)
41
+ end
42
+
43
+ # This enables shortcut xpaths to be used. If no '/' is given, one is appended to the start of the path
44
+ # If attribute value is set then this is also adjusted
45
+ # @return [String] New Xpath adjusted according to any add ons
46
+ def prefix_xpath(xpath, attribute)
47
+ xpath = "//*[@#{attribute}]" unless attribute.nil?
48
+ if xpath[0] != '/'
49
+ xpath = convert_to_pascal_case(xpath) if pascal_keys?
50
+ xpath = '//' + xpath
51
+ end
52
+ xpath
53
+ end
54
+ end
55
+ end
@@ -106,4 +106,4 @@ module Soaspec
106
106
  end
107
107
  end
108
108
  end
109
- end
109
+ end
@@ -6,6 +6,7 @@ require_relative '../core_ext/hash'
6
6
  require_relative '../not_found_errors'
7
7
  require_relative 'handler_accessors'
8
8
  require_relative '../interpreter'
9
+ require_relative 'response_extractor'
9
10
  require 'json'
10
11
  require 'jsonpath'
11
12
  require 'nori'
@@ -14,6 +15,7 @@ require 'erb'
14
15
  module Soaspec
15
16
  # Wraps around Savon client defining default values dependent on the soap request
16
17
  class RestHandler < ExchangeHandler
18
+ include ResponseExtractor
17
19
  extend Soaspec::RestParameters
18
20
  include Soaspec::RestParametersDefaults
19
21
  extend Soaspec::RestExchangeFactory
@@ -25,6 +27,7 @@ module Soaspec
25
27
  # @param [Hash] options Options defining REST request. base_url, default_hash
26
28
  def initialize(name = self.class.to_s, options = {})
27
29
  raise "Base URL not set! Please set in class with 'base_url' method" unless base_url_value
30
+
28
31
  if name.is_a?(Hash) && options == {} # If name is not set, use first parameter as the options hash
29
32
  options = name
30
33
  name = self.class.to_s
@@ -87,6 +90,7 @@ module Soaspec
87
90
  def parse_headers
88
91
  Hash[rest_client_headers.map do |header_name, header_value|
89
92
  raise ArgumentError, "Header '#{header_name}' is null. Headers are #{rest_client_headers}" if header_value.nil?
93
+
90
94
  [header_name, ERB.new(header_value).result(binding)]
91
95
  end]
92
96
  end
@@ -94,6 +98,7 @@ module Soaspec
94
98
  # Convert snakecase to PascalCase
95
99
  def convert_to_pascal_case(key)
96
100
  return key if /[[:upper:]]/ =~ key[0] # If first character already capital, don't do conversion
101
+
97
102
  key.split('_').map(&:capitalize).join
98
103
  end
99
104
 
@@ -104,9 +109,7 @@ module Soaspec
104
109
  options.merge! basic_auth_params if respond_to? :basic_auth_params
105
110
  options[:headers] ||= {}
106
111
  options[:headers].merge! parse_headers
107
- if Soaspec.auto_oauth && respond_to?(:access_token)
108
- options[:headers][:authorization] ||= ERB.new('Bearer <%= access_token %>').result(binding)
109
- end
112
+ options[:headers][:authorization] ||= ERB.new('Bearer <%= access_token %>').result(binding) if Soaspec.auto_oauth && respond_to?(:access_token)
110
113
  options.merge(@init_options)
111
114
  end
112
115
 
@@ -143,16 +146,14 @@ module Soaspec
143
146
 
144
147
  # Returns the value at the provided xpath
145
148
  # @param [RestClient::Response] response
146
- # @param [String] xpath
149
+ # @param [String] xpath Path to find elements from
150
+ # @param [String] attribute Attribute to find path for
147
151
  # @return [Enumerable] Value inside element found through Xpath
148
152
  def xpath_elements_for(response: nil, xpath: nil, attribute: nil)
149
153
  raise ArgumentError unless response && xpath
150
154
  raise "Can't perform XPATH if response is not XML" unless Interpreter.response_type_for(response) == :xml
151
- xpath = "//*[@#{attribute}]" unless attribute.nil?
152
- if xpath[0] != '/'
153
- xpath = convert_to_pascal_case(xpath) if pascal_keys?
154
- xpath = '//' + xpath
155
- end
155
+
156
+ xpath = prefix_xpath(xpath, attribute)
156
157
  temp_doc = Nokogiri.parse(response.body).dup
157
158
  if strip_namespaces? && !xpath.include?(':')
158
159
  temp_doc.remove_namespaces!
@@ -165,6 +166,7 @@ module Soaspec
165
166
  # @return [Enumerable] List of values matching JSON path
166
167
  def json_path_values_for(response, path, attribute: nil)
167
168
  raise 'JSON does not support attributes' if attribute
169
+
168
170
  if path[0] != '$'
169
171
  path = convert_to_pascal_case(path) if pascal_keys?
170
172
  path = '$..' + path
@@ -183,8 +185,9 @@ module Soaspec
183
185
  case Interpreter.response_type_for(response)
184
186
  when :xml
185
187
  result = xpath_elements_for(response: response, xpath: path, attribute: attribute).first
186
- raise NoElementAtPath, "No value at Xpath '#{path}'" unless result
188
+ raise NoElementAtPath, "No value at Xpath '#{prefix_xpath(path, attribute)}'" unless result
187
189
  return result.inner_text if attribute.nil?
190
+
188
191
  return result.attributes[attribute].inner_text
189
192
  when :json
190
193
  paths_to_check = path.split(',')
@@ -192,11 +195,13 @@ module Soaspec
192
195
  json_path_values_for(response, path_to_check, attribute: attribute)
193
196
  end.reject(&:empty?)
194
197
  raise NoElementAtPath, "Path '#{path}' not found in '#{response.body}'" if matching_values.empty?
198
+
195
199
  matching_values.first.first
196
200
  when :hash
197
201
  response.dig(path.split('.')) # Use path as Hash dig expression separating params via '.' TODO: Unit test
198
202
  else
199
203
  raise NoElementAtPath, 'Response is empty' if response.to_s.empty?
204
+
200
205
  response.to_s[/path/] # Perform regular expression using path if not XML nor JSON TODO: Unit test
201
206
  end
202
207
  end
@@ -214,42 +219,10 @@ module Soaspec
214
219
  end
215
220
  end
216
221
 
217
- # TODO: This and 'to_hash' method should be merged
218
- # Convert XML or JSON response into a Hash
219
- # @param [String] response Response as a String (either in XML or JSON)
220
- # @return [Hash]
221
- def extract_hash(response)
222
- raise NoElementAtPath, "Empty Body. Can't assert on it" if response.body.empty?
223
- case Interpreter.response_type_for response
224
- when :json
225
- converted = JSON.parse(response.body)
226
- return converted.transform_keys_to_symbols if converted.is_a? Hash
227
- return converted.map!(&:transform_keys_to_symbols) if converted.is_a? Array
228
- raise 'Incorrect Type produced ' + converted.class
229
- when :xml
230
- parser = Nori.new(convert_tags_to: lambda { |tag| tag.snakecase.to_sym })
231
- parser.parse(response.body)
232
- else
233
- raise "Neither XML nor JSON detected. It is #{type}. Don't know how to parse It is #{response.body}"
234
- end
235
- end
236
-
237
- # @return [Hash] Hash representing response body
238
- def to_hash(response)
239
- case Interpreter.response_type_for(response)
240
- when :xml
241
- parser = Nori.new(strip_namespaces: strip_namespaces?, convert_tags_to: ->(tag) { tag.snakecase.to_sym })
242
- parser.parse(response.body.to_s)
243
- when :json
244
- JSON.parse(response.body.to_s)
245
- else
246
- raise "Unable to interpret type of #{response.body}"
247
- end
248
- end
249
-
250
222
  # @response [RestClient::Request] Request of API call. Either intended request or actual request
251
223
  def request(response)
252
224
  return 'Request not yet sent' if response.nil?
225
+
253
226
  response.request
254
227
  end
255
228
 
@@ -1,4 +1,3 @@
1
-
2
1
  module Soaspec
3
2
  # Contains commonly used REST methods
4
3
  module RestMethods
@@ -42,4 +41,4 @@ module Soaspec
42
41
  Exchange.new(name, method: :delete, **params)
43
42
  end
44
43
  end
45
- end
44
+ end
@@ -1,11 +1,11 @@
1
1
  module Soaspec
2
2
  # Methods to define parameters specific to REST handler
3
3
  module RestParameters
4
-
5
4
  # Defines method 'base_url_value' containing base URL used in REST requests
6
5
  # @param [String] url Base Url to use in REST requests. Suburl is appended to this
7
6
  def base_url(url)
8
7
  raise ArgumentError, "Base Url passed must be a String for #{self} but was #{url.class}" unless url.is_a?(String)
8
+
9
9
  define_method('base_url_value') { ERB.new(url).result(binding) }
10
10
  end
11
11
 
@@ -37,6 +37,7 @@ module Soaspec
37
37
  # @return [String] Client id obtained from credentials file
38
38
  def client_id
39
39
  raise '@client_id is not set. Set by specifying credentials file with "oauth2_file FILENAME" before this is called' unless @client_id
40
+
40
41
  @client_id
41
42
  end
42
43
 
@@ -45,6 +46,7 @@ module Soaspec
45
46
  # @param [String] password Password to use
46
47
  def basic_auth(user: nil, password: nil)
47
48
  raise ArgumentError, "Must pass both 'user' and 'password' for #{self}" unless user && password
49
+
48
50
  define_method('basic_auth_params') do
49
51
  { user: user, password: password }
50
52
  end
@@ -72,11 +74,13 @@ module Soaspec
72
74
  # @return [Hash] Hash with credentials in it
73
75
  def load_credentials_hash(filename)
74
76
  raise ArgumentError, "Filename passed must be a String for #{self} but was #{filename.class}" unless filename.is_a?(String)
77
+
75
78
  full_path = Soaspec.credentials_folder ? File.join(Soaspec.credentials_folder, filename) : filename
76
79
  full_path += '.yml' unless full_path.end_with?('.yml') # Automatically add 'yml' extension
77
80
  file_hash = YAML.load_file(full_path)
78
81
  raise "File at #{full_path} is not a hash" unless file_hash.is_a? Hash
82
+
79
83
  file_hash.transform_keys_to_symbols
80
84
  end
81
85
  end
82
- end
86
+ end
@@ -18,4 +18,4 @@ module Soaspec
18
18
  false
19
19
  end
20
20
  end
21
- end
21
+ end
@@ -1,4 +1,3 @@
1
-
2
1
  require_relative 'exchange_handler'
3
2
  require_relative '../core_ext/hash'
4
3
  require_relative '../not_found_errors'
@@ -7,7 +6,6 @@ require_relative '../interpreter'
7
6
  require 'forwardable'
8
7
 
9
8
  module Soaspec
10
-
11
9
  # Accessors specific to SOAP handler
12
10
  module SoapAccessors
13
11
  # Define attributes set on root SOAP element
@@ -78,10 +76,8 @@ module Soaspec
78
76
  name = self.class.to_s
79
77
  end
80
78
  super
81
- set_remove_key(options, :operation)
82
- set_remove_key(options, :default_hash)
83
- set_remove_key(options, :template_name)
84
- merged_options = Soaspec.log_api_traffic? ? default_options.merge(logging_options) : default_options
79
+ set_remove_keys(options, %i[operation default_hash template_name])
80
+ merged_options = Soaspec::SpecLogger.log_api_traffic? ? default_options.merge(logging_options) : default_options
85
81
  merged_options.merge! savon_options
86
82
  merged_options.merge!(options)
87
83
  self.client = Savon.client(merged_options)
@@ -152,7 +148,7 @@ module Soaspec
152
148
  # @param [Nokogiri::XML::Document]
153
149
  def convert_to_lower_case(xml_doc)
154
150
  xml_doc.traverse do |node|
155
- node.name = node.name.downcase if node.kind_of?(Nokogiri::XML::Element)
151
+ node.name = node.name.downcase if node.is_a?(Nokogiri::XML::Element)
156
152
  end
157
153
  end
158
154
 
@@ -162,6 +158,7 @@ module Soaspec
162
158
  # @return [Enumerable] Elements found through Xpath
163
159
  def xpath_elements_for(response: nil, xpath: nil, attribute: nil)
164
160
  raise ArgumentError('response and xpath must be passed to method') unless response && xpath
161
+
165
162
  xpath = "//*[@#{attribute}]" unless attribute.nil?
166
163
  xpath = '//' + xpath if xpath[0] != '/'
167
164
  temp_doc = response.doc.dup
@@ -184,6 +181,7 @@ module Soaspec
184
181
  results = xpath_elements_for(response: response, xpath: path, attribute: attribute)
185
182
  raise NoElementAtPath, "No value at Xpath '#{path}' in XML #{response.doc}" if results.empty?
186
183
  return results.first.inner_text if attribute.nil?
184
+
187
185
  results.first.attributes[attribute].inner_text
188
186
  end
189
187
 
@@ -206,7 +204,6 @@ module Soaspec
206
204
 
207
205
  # Convenience methods for once off usage of a SOAP request
208
206
  class << self
209
-
210
207
  # Implement undefined setter with []= for FactoryBot to use without needing to define params to set
211
208
  # @param [Object] method_name Name of method not defined
212
209
  # @param [Object] args Arguments passed to method
@@ -231,6 +228,5 @@ module Soaspec
231
228
  operations.include?(method_name) || super
232
229
  end
233
230
  end
234
-
235
231
  end
236
- end
232
+ end
@@ -1,7 +1,6 @@
1
1
  # Convenience methods to set Exchange specific properties
2
2
  # Will be used when creating a subclass of Exchange
3
3
  module ExchangeProperties
4
-
5
4
  # Set default exchange handler for this exchange
6
5
  # This is helpful for when you need a new exchange handler created for each exchange
7
6
  # @param [Class] handler_class Class of ExchangeHandler to set Exchange to use
@@ -25,4 +24,4 @@ module ExchangeProperties
25
24
  true
26
25
  end
27
26
  end
28
- end
27
+ end
@@ -1,9 +1,7 @@
1
-
2
1
  require 'fileutils'
3
2
  module Soaspec
4
3
  # Help with tasks common to soaspec executables
5
4
  module ExeHelpers
6
-
7
5
  # Create files in project depending on type of project
8
6
  # @param [String] type Type of project to create, e.g., 'soap', 'rest'
9
7
  def create_files_for(type)
@@ -19,9 +17,10 @@ module Soaspec
19
17
  end
20
18
 
21
19
  # @param [Array] filenames List of files to create
22
- def create_files(filenames)
20
+ def create_files(filenames, ignore_if_present: false)
23
21
  raise ArgumentError, 'Expected filenames to be an Array' unless filenames.is_a? Array
24
- filenames.each { |name| create_file filename: name }
22
+
23
+ filenames.each { |name| create_file filename: name, ignore_if_present: ignore_if_present }
25
24
  end
26
25
 
27
26
  # Spec task string depending upon whether virtual is used
@@ -45,15 +44,15 @@ module Soaspec
45
44
 
46
45
  # @param [String] filename Name of the file to create
47
46
  # @param [String] content Content to place inside file
47
+ # @param [Boolean] ignore_if_present Don't complain if file is present
48
48
  def create_file(filename: nil, content: nil, ignore_if_present: false, erb: true)
49
49
  raise 'Need to pass filename' unless filename
50
+
50
51
  content ||= retrieve_contents(filename, erb)
51
52
  create_folder File.split(filename).first
52
53
  if File.exist? filename
53
54
  old_content = File.read(filename)
54
- if old_content != content && !ignore_if_present
55
- warn "!! #{filename} already exists and differs from template"
56
- end
55
+ warn "!! #{filename} already exists and differs from template" if old_content != content && !ignore_if_present
57
56
  else
58
57
  File.open(filename, 'w') { |f| f.puts content }
59
58
  puts 'Created: ' + filename
@@ -82,6 +81,5 @@ module Soaspec
82
81
  def generated_soap_spec_for(operation)
83
82
  ERB.new(File.read(File.join(File.dirname(__FILE__), 'generator', 'spec/dynamic_soap_spec.rb.erb'))).result(binding)
84
83
  end
85
-
86
84
  end
87
- end
85
+ end
@@ -4,4 +4,4 @@ require 'hashie'
4
4
  class IndifferentHash < Hash
5
5
  include Hashie::Extensions::MergeInitializer
6
6
  include Hashie::Extensions::IndifferentAccess
7
- end
7
+ end
@@ -1,7 +1,5 @@
1
-
2
1
  # Help interpret the general type of a particular object
3
2
  class Interpreter
4
-
5
3
  class << self
6
4
  # @param [Object] response API response
7
5
  # @return [Symbol] Type of provided response
@@ -38,4 +36,4 @@ class Interpreter
38
36
  false
39
37
  end
40
38
  end
41
- end
39
+ end
@@ -1,4 +1,3 @@
1
-
2
1
  require_relative 'core_ext/hash'
3
2
  require_relative 'not_found_errors'
4
3
 
@@ -61,7 +60,6 @@ end
61
60
  RSpec::Matchers.alias_matcher :have_jsonpath_value, :have_xpath_value
62
61
 
63
62
  RSpec::Matchers.define :be_found do
64
-
65
63
  match do |exchange|
66
64
  expect(exchange.exchange_handler.found?(exchange.response)).to be true
67
65
  end
@@ -91,6 +89,7 @@ RSpec::Matchers.define :be_successful do
91
89
  failure_list << "Expected value at json '#{path}' to be '#{value}' but was '#{exchange[path]}'" unless exchange[path] == value
92
90
  end
93
91
  raise failure_list.to_s unless failure_list.empty?
92
+
94
93
  true
95
94
  end
96
95
  end
@@ -38,6 +38,7 @@ module Soaspec
38
38
  params[:token_url] ||= Soaspec::OAuth2.token_url
39
39
  raise ArgumentError, 'client_id and client_secret not set' unless params[:client_id] && params[:client_secret]
40
40
  raise ArgumentError, 'token_url mandatory' unless params[:token_url]
41
+
41
42
  self.params = params
42
43
  params[:username] = api_username || ERB.new(params[:username]).result(binding) if params[:username]
43
44
  params[:security_token] = ERB.new(params[:security_token]).result(binding) if params[:security_token]
@@ -99,4 +100,4 @@ module Soaspec
99
100
  end)
100
101
  end
101
102
  end
102
- end
103
+ end