soaspec 0.2.8 → 0.2.9

Sign up to get free protection for your applications and to get access to all the features.
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