soaspec 0.0.81 → 0.0.82

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 (59) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +15 -15
  3. data/.gitlab-ci.yml +31 -31
  4. data/.rspec +3 -3
  5. data/.rubocop.yml +2 -2
  6. data/.travis.yml +5 -5
  7. data/CODE_OF_CONDUCT.md +74 -74
  8. data/ChangeLog +345 -341
  9. data/Gemfile +6 -6
  10. data/LICENSE.txt +21 -21
  11. data/README.md +85 -85
  12. data/Rakefile +24 -24
  13. data/Todo.md +6 -6
  14. data/exe/soaspec +123 -123
  15. data/exe/soaspec-virtual-server +98 -92
  16. data/exe/xml_to_yaml_file +60 -60
  17. data/lib/soaspec.rb +86 -85
  18. data/lib/soaspec/core_ext/hash.rb +83 -83
  19. data/lib/soaspec/exchange.rb +230 -230
  20. data/lib/soaspec/exchange_handlers/exchange_handler.rb +98 -98
  21. data/lib/soaspec/exchange_handlers/handler_accessors.rb +95 -95
  22. data/lib/soaspec/exchange_handlers/rest_handler.rb +367 -367
  23. data/lib/soaspec/exchange_handlers/rest_methods.rb +44 -44
  24. data/lib/soaspec/exchange_handlers/soap_handler.rb +231 -231
  25. data/lib/soaspec/exe_helpers.rb +56 -56
  26. data/lib/soaspec/generator/.rspec.erb +5 -5
  27. data/lib/soaspec/generator/.travis.yml.erb +5 -5
  28. data/lib/soaspec/generator/Gemfile.erb +8 -8
  29. data/lib/soaspec/generator/README.md.erb +29 -29
  30. data/lib/soaspec/generator/Rakefile.erb +19 -19
  31. data/lib/soaspec/generator/config/data/default.yml.erb +1 -1
  32. data/lib/soaspec/generator/lib/blz_service.rb.erb +24 -24
  33. data/lib/soaspec/generator/lib/dynamic_class_content.rb.erb +12 -12
  34. data/lib/soaspec/generator/lib/shared_example.rb.erb +8 -8
  35. data/lib/soaspec/generator/spec/dynamic_soap_spec.rb.erb +14 -14
  36. data/lib/soaspec/generator/spec/soap_spec.rb.erb +53 -53
  37. data/lib/soaspec/generator/spec/spec_helper.rb.erb +20 -20
  38. data/lib/soaspec/generator/template/soap_template.xml +6 -6
  39. data/lib/soaspec/interpreter.rb +25 -25
  40. data/lib/soaspec/matchers.rb +65 -65
  41. data/lib/soaspec/not_found_errors.rb +13 -13
  42. data/lib/soaspec/soaspec_shared_examples.rb +24 -24
  43. data/lib/soaspec/spec_logger.rb +27 -27
  44. data/lib/soaspec/test_server/bank.wsdl +90 -90
  45. data/lib/soaspec/test_server/get_bank.rb +160 -160
  46. data/lib/soaspec/test_server/invoices.rb +27 -27
  47. data/lib/soaspec/test_server/namespace.xml +15 -0
  48. data/lib/soaspec/test_server/note.xml +5 -5
  49. data/lib/soaspec/test_server/puppy_service.rb +20 -20
  50. data/lib/soaspec/test_server/test_attribute.rb +13 -13
  51. data/lib/soaspec/test_server/test_namespace.rb +13 -0
  52. data/lib/soaspec/version.rb +2 -2
  53. data/lib/soaspec/wsdl_generator.rb +93 -93
  54. data/soaspec.gemspec +45 -45
  55. data/test.wsdl +116 -116
  56. data/test.xml +10 -10
  57. data/test_rest.rb +97 -97
  58. data/test_wsdl.rb +43 -43
  59. metadata +5 -3
@@ -1,99 +1,99 @@
1
-
2
- require_relative 'handler_accessors'
3
-
4
- module Soaspec
5
-
6
- # Inherit this for a class describing how to implement a particular exchange.
7
- # Has basic methods common for methods defining RSpec tests in YAML
8
- class ExchangeHandler
9
- extend Soaspec::HandlerAccessors
10
-
11
- # Retrieve the name of the template file to be used in the API request
12
- attr_reader :template_name
13
-
14
- # Explicitly defined elements for which a path has been predefined
15
- def elements
16
- public_methods.select { |i| i[/__custom_path_.+/] }
17
- end
18
-
19
- # Set the default hash representing data to be used in making a request
20
- # This will set the @request_option instance variable too
21
- def default_hash=(hash)
22
- @request_option = :hash
23
- @default_hash = Soaspec.always_use_keys? ? hash.transform_keys_to_symbols : hash
24
- end
25
-
26
- # Set instance variable name
27
- # @param [String, Symbol] name Name used when describing API test
28
- # @param [Hash] options Parameters defining handler. Used in descendants
29
- def initialize(name = self.class.to_s, options = {})
30
- use
31
- @name = name
32
- end
33
-
34
- # Set Api handler used by Exchange class to this handler
35
- # @return [Self]
36
- def use
37
- Soaspec.api_handler = self
38
- self
39
- end
40
-
41
- # Sets api handler variable globally. This is used in 'Exchange' class
42
- # @return [String] Name set upon initialisation
43
- def to_s
44
- use
45
- @name.to_s
46
- end
47
-
48
- # Set the request option type and the template name
49
- # Erb is used to parse the template file, executing Ruby code in `<%= %>` blocks to work out the final request
50
- # @param [String] name Name of file inside 'template' folder excluding extension
51
- def template_name=(name)
52
- @request_option = :template
53
- @template_name = name
54
- end
55
-
56
- # Will be used in 'success_scenarios' shared examples.
57
- # Set though 'mandatory_elements' method
58
- # @return [Array] Array of symbols specifying element names
59
- def expected_mandatory_elements
60
- []
61
- end
62
-
63
- # Change this through 'mandatory_xpath_values' method to specify xpath results that must be present in the response
64
- # Will be used in 'success_scenarios' shared examples
65
- # @return [Hash] Hash of 'xpath' => 'expected value' pairs
66
- def expected_mandatory_xpath_values
67
- {}
68
- end
69
-
70
- # Change this through 'mandatory_json_values' method to specify json results that must be present in the response
71
- # Will be used in 'success_scenarios' shared examples
72
- # @return [Hash] Hash of 'json/path' => 'expected value' pairs
73
- def expected_mandatory_json_values
74
- {}
75
- end
76
-
77
- # @return [Boolean] Whether all xpaths will be done with XML that is converted to lower case
78
- def convert_to_lower?
79
- false
80
- end
81
-
82
- # Stores a value in a method that can be accessed by the provided name
83
- # @param [Symbol] name Name of method to use to access this value within handler
84
- # @param [String] value Value to store
85
- def store(name, value)
86
- define_singleton_method('__stored_val__' + name.to_s) do
87
- value
88
- end
89
- end
90
-
91
- # Set instance variable and remove it from Hash
92
- def set_remove_key(hash, key)
93
- return unless hash.key? key
94
- __send__("#{key}=", hash[key])
95
- hash.delete key
96
- end
97
-
98
- end
1
+
2
+ require_relative 'handler_accessors'
3
+
4
+ module Soaspec
5
+
6
+ # Inherit this for a class describing how to implement a particular exchange.
7
+ # Has basic methods common for methods defining RSpec tests in YAML
8
+ class ExchangeHandler
9
+ extend Soaspec::HandlerAccessors
10
+
11
+ # Retrieve the name of the template file to be used in the API request
12
+ attr_reader :template_name
13
+
14
+ # Explicitly defined elements for which a path has been predefined
15
+ def elements
16
+ public_methods.select { |i| i[/__custom_path_.+/] }
17
+ end
18
+
19
+ # Set the default hash representing data to be used in making a request
20
+ # This will set the @request_option instance variable too
21
+ def default_hash=(hash)
22
+ @request_option = :hash
23
+ @default_hash = Soaspec.always_use_keys? ? hash.transform_keys_to_symbols : hash
24
+ end
25
+
26
+ # Set instance variable name
27
+ # @param [String, Symbol] name Name used when describing API test
28
+ # @param [Hash] options Parameters defining handler. Used in descendants
29
+ def initialize(name = self.class.to_s, options = {})
30
+ use
31
+ @name = name
32
+ end
33
+
34
+ # Set Api handler used by Exchange class to this handler
35
+ # @return [Self]
36
+ def use
37
+ Soaspec.api_handler = self
38
+ self
39
+ end
40
+
41
+ # Sets api handler variable globally. This is used in 'Exchange' class
42
+ # @return [String] Name set upon initialisation
43
+ def to_s
44
+ use
45
+ @name.to_s
46
+ end
47
+
48
+ # Set the request option type and the template name
49
+ # Erb is used to parse the template file, executing Ruby code in `<%= %>` blocks to work out the final request
50
+ # @param [String] name Name of file inside 'template' folder excluding extension
51
+ def template_name=(name)
52
+ @request_option = :template
53
+ @template_name = name
54
+ end
55
+
56
+ # Will be used in 'success_scenarios' shared examples.
57
+ # Set though 'mandatory_elements' method
58
+ # @return [Array] Array of symbols specifying element names
59
+ def expected_mandatory_elements
60
+ []
61
+ end
62
+
63
+ # Change this through 'mandatory_xpath_values' method to specify xpath results that must be present in the response
64
+ # Will be used in 'success_scenarios' shared examples
65
+ # @return [Hash] Hash of 'xpath' => 'expected value' pairs
66
+ def expected_mandatory_xpath_values
67
+ {}
68
+ end
69
+
70
+ # Change this through 'mandatory_json_values' method to specify json results that must be present in the response
71
+ # Will be used in 'success_scenarios' shared examples
72
+ # @return [Hash] Hash of 'json/path' => 'expected value' pairs
73
+ def expected_mandatory_json_values
74
+ {}
75
+ end
76
+
77
+ # @return [Boolean] Whether all xpaths will be done with XML that is converted to lower case
78
+ def convert_to_lower?
79
+ false
80
+ end
81
+
82
+ # Stores a value in a method that can be accessed by the provided name
83
+ # @param [Symbol] name Name of method to use to access this value within handler
84
+ # @param [String] value Value to store
85
+ def store(name, value)
86
+ define_singleton_method('__stored_val__' + name.to_s) do
87
+ value
88
+ end
89
+ end
90
+
91
+ # Set instance variable and remove it from Hash
92
+ def set_remove_key(hash, key)
93
+ return unless hash.key? key
94
+ __send__("#{key}=", hash[key])
95
+ hash.delete key
96
+ end
97
+
98
+ end
99
99
  end
@@ -1,96 +1,96 @@
1
- module Soaspec
2
- # Describes methods test handlers use to easily set attributes
3
- # Some are included in 'success scenarios' and to configure the request sent
4
- module HandlerAccessors
5
-
6
- # Defines expected_mandatory_elements method used in 'success_scenario' shared examples
7
- # to indicate certain elements must be present
8
- # @param [Array] elements Array of symbols specifying expected element names for 'success scenario' in snakecase
9
- #
10
- # @example Inside class
11
- # mandatory_elements :GetWeatherResult
12
- #
13
- # Or for a list
14
- #
15
- # @example Inside class
16
- # mandatory_elements [:GetWeatherResult, :GetResultStatus]
17
- #
18
- # In test
19
- # describe Exchange(:name) do
20
- # it_behaves_like 'success scenario' # Includes checks for mandatory elements
21
- # end
22
- def mandatory_elements(elements)
23
- define_method('expected_mandatory_elements') do
24
- return [elements] if elements.is_a?(String) || elements.is_a?(Symbol)
25
- elements
26
- end
27
- end
28
-
29
- # Defines mandatory xpaths value pairs to be included in 'success scenario' shared example
30
- #
31
- # @example Inside class
32
- # mandatory_xpath_values '//xmlns:GetWeatherResult' => 'Data Not Found'
33
- #
34
- # In test
35
- # describe Exchange(:name) do
36
- # it_behaves_like 'success scenario' # Includes xpath pair validation
37
- # end
38
- #
39
- def mandatory_xpath_values(xpath_value_pairs)
40
- raise ArgumentError('Hash of {xpath => expected values} expected ') unless xpath_value_pairs.is_a? Hash
41
- define_method('expected_mandatory_xpath_values') do
42
- xpath_value_pairs
43
- end
44
- end
45
-
46
- # Defines mandatory json path value pairs to be included in 'success scenario' shared example
47
- #
48
- # @example Inside class
49
- # mandatory_json_values '$..GetWeatherResult' => 'Found'
50
- #
51
- # In test
52
- # describe Exchange(:name) do
53
- # it_behaves_like 'success scenario' # Includes json pair validation
54
- # end
55
- #
56
- def mandatory_json_values(json_value_pairs)
57
- raise ArgumentError("Hash of {'jsonpath' => expected values} expected") unless json_value_pairs.is_a? Hash
58
- define_method('expected_mandatory_json_values') do
59
- json_value_pairs
60
- end
61
- end
62
-
63
- # Links a particular path to a meaningful method that can be accessed from Exchange class.
64
- # This will use the 'value_from_path' method which
65
- # should be implemented by each ExchangeHandler
66
- # @param [String, Symbol] name Method name used to access element
67
- # @param [String, Symbol] path Path to find object (e.g, XPath, JSONPath)
68
- def element(name, path)
69
- define_method("__custom_path_#{name}") do |response|
70
- value_from_path(response, path.to_s)
71
- end
72
- end
73
-
74
- # Links an attribute to a meaningful method that can be accessed from Exchange class.
75
- # This will use the 'value_from_path' method which
76
- # should be implemented by each ExchangeHandler
77
- # @param [String, Symbol] name Method name used to access attribute
78
- # @param [String, nil, Hash] attribute Attribute name. If not set, this will default to @name
79
- def attribute(name, attribute = nil)
80
- attribute_used = attribute ? attribute : name.to_s
81
- define_method("__custom_path_#{name}") do |response|
82
- value_from_path(response, 'implicit', attribute: attribute_used)
83
- end
84
- end
85
-
86
- # All xpath will be done with XML that is converted to lower case
87
- # You must then use lower case in the xpath's to obtain the desired values
88
- def convert_to_lower(set)
89
- return unless set
90
- define_method('convert_to_lower?') do
91
- true
92
- end
93
- end
94
-
95
- end
1
+ module Soaspec
2
+ # Describes methods test handlers use to easily set attributes
3
+ # Some are included in 'success scenarios' and to configure the request sent
4
+ module HandlerAccessors
5
+
6
+ # Defines expected_mandatory_elements method used in 'success_scenario' shared examples
7
+ # to indicate certain elements must be present
8
+ # @param [Array] elements Array of symbols specifying expected element names for 'success scenario' in snakecase
9
+ #
10
+ # @example Inside class
11
+ # mandatory_elements :GetWeatherResult
12
+ #
13
+ # Or for a list
14
+ #
15
+ # @example Inside class
16
+ # mandatory_elements [:GetWeatherResult, :GetResultStatus]
17
+ #
18
+ # In test
19
+ # describe Exchange(:name) do
20
+ # it_behaves_like 'success scenario' # Includes checks for mandatory elements
21
+ # end
22
+ def mandatory_elements(elements)
23
+ define_method('expected_mandatory_elements') do
24
+ return [elements] if elements.is_a?(String) || elements.is_a?(Symbol)
25
+ elements
26
+ end
27
+ end
28
+
29
+ # Defines mandatory xpaths value pairs to be included in 'success scenario' shared example
30
+ #
31
+ # @example Inside class
32
+ # mandatory_xpath_values '//xmlns:GetWeatherResult' => 'Data Not Found'
33
+ #
34
+ # In test
35
+ # describe Exchange(:name) do
36
+ # it_behaves_like 'success scenario' # Includes xpath pair validation
37
+ # end
38
+ #
39
+ def mandatory_xpath_values(xpath_value_pairs)
40
+ raise ArgumentError('Hash of {xpath => expected values} expected ') unless xpath_value_pairs.is_a? Hash
41
+ define_method('expected_mandatory_xpath_values') do
42
+ xpath_value_pairs
43
+ end
44
+ end
45
+
46
+ # Defines mandatory json path value pairs to be included in 'success scenario' shared example
47
+ #
48
+ # @example Inside class
49
+ # mandatory_json_values '$..GetWeatherResult' => 'Found'
50
+ #
51
+ # In test
52
+ # describe Exchange(:name) do
53
+ # it_behaves_like 'success scenario' # Includes json pair validation
54
+ # end
55
+ #
56
+ def mandatory_json_values(json_value_pairs)
57
+ raise ArgumentError("Hash of {'jsonpath' => expected values} expected") unless json_value_pairs.is_a? Hash
58
+ define_method('expected_mandatory_json_values') do
59
+ json_value_pairs
60
+ end
61
+ end
62
+
63
+ # Links a particular path to a meaningful method that can be accessed from Exchange class.
64
+ # This will use the 'value_from_path' method which
65
+ # should be implemented by each ExchangeHandler
66
+ # @param [String, Symbol] name Method name used to access element
67
+ # @param [String, Symbol] path Path to find object (e.g, XPath, JSONPath)
68
+ def element(name, path)
69
+ define_method("__custom_path_#{name}") do |response|
70
+ value_from_path(response, path.to_s)
71
+ end
72
+ end
73
+
74
+ # Links an attribute to a meaningful method that can be accessed from Exchange class.
75
+ # This will use the 'value_from_path' method which
76
+ # should be implemented by each ExchangeHandler
77
+ # @param [String, Symbol] name Method name used to access attribute
78
+ # @param [String, nil, Hash] attribute Attribute name. If not set, this will default to @name
79
+ def attribute(name, attribute = nil)
80
+ attribute_used = attribute ? attribute : name.to_s
81
+ define_method("__custom_path_#{name}") do |response|
82
+ value_from_path(response, 'implicit', attribute: attribute_used)
83
+ end
84
+ end
85
+
86
+ # All xpath will be done with XML that is converted to lower case
87
+ # You must then use lower case in the xpath's to obtain the desired values
88
+ def convert_to_lower(set)
89
+ return unless set
90
+ define_method('convert_to_lower?') do
91
+ true
92
+ end
93
+ end
94
+
95
+ end
96
96
  end
@@ -1,368 +1,368 @@
1
-
2
- require_relative 'exchange_handler'
3
- require_relative '../core_ext/hash'
4
- require_relative '../not_found_errors'
5
- require_relative 'handler_accessors'
6
- require_relative '../interpreter'
7
- require 'json'
8
- require 'jsonpath'
9
- require 'nori'
10
- require 'erb'
11
-
12
- module Soaspec
13
-
14
- # Accessors specific to REST handler
15
- module RestAccessors
16
-
17
- # Defines method 'base_url_value' containing base URL used in REST requests
18
- # @param [String] url Base Url to use in REST requests. Suburl is appended to this
19
- def base_url(url)
20
- define_method('base_url_value') do
21
- url
22
- end
23
- end
24
-
25
- # Will create access_token method based on passed parameters
26
- def oauth2(client_id: nil, client_secret: nil, token_url: nil, username: nil, password: nil, security_token: nil)
27
- define_method('oauth_response') do
28
- username = api_username || ERB.new(username).result(binding) if username
29
- security_token = ERB.new(security_token).result(binding) if security_token
30
- token_url = ERB.new(token_url).result(binding) if token_url
31
- password = ERB.new(password).result(binding) if password
32
- payload = if password && username
33
- {
34
- grant_type: 'password',
35
- client_id: client_id,
36
- client_secret: client_secret,
37
- username: username,
38
- password: security_token ? (password + security_token) : password,
39
- multipart: true
40
- }
41
- else
42
- {
43
- grant_type: 'client_credentials',
44
- client_id: client_id,
45
- client_secret: client_secret
46
- }
47
- end
48
- retry_count = 0
49
- begin
50
- Soaspec::SpecLogger.add_to 'request_params: ' + payload.to_s
51
- response = RestClient.post(token_url, payload, cache_control: 'no_cache', verify_ssl: false)
52
- rescue RestClient::Exception => e
53
- Soaspec::SpecLogger.add_to("oauth_error: #{e.message}")
54
- Soaspec::SpecLogger.add_to("oauth_error: #{e.response}")
55
- retry_count += 1
56
- retry if retry_count < 3
57
- raise e
58
- end
59
- Soaspec::SpecLogger.add_to("response_headers: #{response.headers}")
60
- Soaspec::SpecLogger.add_to("response_body: #{response.body}")
61
- JSON.parse(response)
62
- end
63
-
64
- define_method('access_token') do
65
- oauth_response['access_token']
66
- end
67
- define_method('instance_url') do
68
- oauth_response['instance_url']
69
- end
70
- end
71
-
72
- # Pass path to YAML file containing OAuth2 parameters
73
- # @param [String] path_to_filename Will have Soaspec.credentials_folder appended to it if set
74
- def oauth2_file(path_to_filename)
75
- full_path = Soaspec.credentials_folder ? File.join(Soaspec.credentials_folder, path_to_filename + '.yml') : path_to_filename + '.yml'
76
- file_hash = YAML.load_file(full_path)
77
- raise 'File at ' + full_path + ' is not a hash ' unless file_hash.is_a? Hash
78
- oauth_hash = file_hash.transform_keys_to_symbols
79
- oauth2 **oauth_hash
80
- end
81
-
82
- # @param [Hash] headers Hash of REST headers used in RestClient
83
- def headers(headers)
84
- define_method('rest_client_headers') do
85
- headers
86
- end
87
- end
88
-
89
- # Convert each key from snake_case to PascalCase
90
- def pascal_keys(set)
91
- define_method('pascal_keys?') do
92
- set
93
- end
94
- end
95
-
96
- end
97
-
98
- # Wraps around Savon client defining default values dependent on the soap request
99
- class RestHandler < ExchangeHandler
100
- extend Soaspec::RestAccessors
101
-
102
- # User used in making API calls
103
- attr_accessor :api_username
104
-
105
- # Set through following method. Base URL in REST requests.
106
- def base_url_value
107
- nil
108
- end
109
-
110
- # Headers used in RestClient
111
- def rest_client_headers
112
- {}
113
- end
114
-
115
- # Add values to here when extending this class to have default REST options.
116
- # See rest client resource at https://github.com/rest-client/rest-client for details
117
- # It's easier to set headers via 'headers' accessor rather than here
118
- # @return [Hash] Options adding to & overriding defaults
119
- def rest_resource_options
120
- {
121
- }
122
- end
123
-
124
- # Perform ERB on each header value
125
- # @return [Hash] Hash from 'rest_client_headers' passed through ERB
126
- def parse_headers
127
- Hash[rest_client_headers.map { |k, header| [k, ERB.new(header).result(binding)] }]
128
- end
129
-
130
- # Setup object to handle communicating with a particular SOAP WSDL
131
- # @param [Hash] options Options defining SOAP request. WSDL, authentication
132
- def initialize(name = self.class.to_s, options = {})
133
- raise "Base URL not set! Please set in class with 'base_url' method" unless base_url_value
134
- @default_hash = {}
135
- if name.is_a?(Hash) && options == {} # If name is not set
136
- options = name
137
- name = self.class.to_s
138
- end
139
- super
140
- set_remove_key(options, :api_username)
141
- set_remove_key(options, :default_hash)
142
- @init_options = options
143
- end
144
-
145
- # Convert snakecase to PascalCase
146
- def convert_to_pascal_case(key)
147
- return key if /[[:upper:]]/ =~ key[0] # If first character already capital, don't do conversion
148
- key.split('_').map(&:capitalize).join
149
- end
150
-
151
- # Whether to convert each key in the request to PascalCase
152
- # It will also auto convert simple XPath, JSONPath where '//' or '..' not specified
153
- # @return Whether to convert to PascalCase
154
- def pascal_keys?
155
- false
156
- end
157
-
158
- # @return [Hash]
159
- def hash_used_in_request(override_hash)
160
- request = @default_hash.merge(override_hash)
161
- if pascal_keys?
162
- request.map { |k, v| [convert_to_pascal_case(k.to_s), v] }.to_h
163
- else
164
- request
165
- end
166
- end
167
-
168
- # Initialize value of merged options
169
- def init_merge_options
170
- options = rest_resource_options
171
- options[:headers] ||= {}
172
- options[:headers].merge! parse_headers
173
- options.merge(@init_options)
174
- end
175
-
176
- # Used in together with Exchange request that passes such override parameters
177
- # @param [Hash] override_parameters Params to characterize REST request
178
- # @param_value [params] Extra parameters (E.g. headers)
179
- # @param_value [suburl] URL appended to base_url of class
180
- # @param_value [method] REST method (:get, :post, etc)
181
- def make_request(override_parameters)
182
- @merged_options ||= init_merge_options
183
- test_values = override_parameters
184
- test_values[:params] ||= {}
185
- test_values[:method] ||= :post
186
- test_values[:suburl] = test_values[:suburl].to_s if test_values[:suburl]
187
- test_values[:params][:params] = test_values[:q] if test_values[:q] # Use q for query parameters. Nested :params is ugly and long
188
-
189
- # In order for ERB to be calculated at correct time, the first time request is made, the resource should be created
190
- @resource ||= RestClient::Resource.new(ERB.new(base_url_value).result(binding), @merged_options)
191
-
192
- @resource_used = test_values[:suburl] ? @resource[test_values[:suburl]] : @resource
193
-
194
- begin
195
- response = case test_values[:method]
196
- when :post, :patch, :put
197
- unless test_values[:payload]
198
- test_values[:payload] = JSON.generate(hash_used_in_request(test_values[:body])).to_s if test_values[:body]
199
- end
200
- @resource_used.send(test_values[:method].to_s, test_values[:payload], test_values[:params])
201
- else
202
- @resource_used.send(test_values[:method].to_s, test_values[:params])
203
- end
204
- rescue RestClient::ExceptionWithResponse => e
205
- response = e.response
206
- end
207
- Soaspec::SpecLogger.add_to('response_headers: ' + response.headers.to_s)
208
- Soaspec::SpecLogger.add_to('response_body: ' + response.to_s)
209
- response
210
- end
211
-
212
- # @param [Hash] _format Format of expected result. Ignored for this
213
- # @return [Object] Generic body to be displayed in error messages
214
- def response_body(response, _format: :hash)
215
- extract_hash response
216
- end
217
-
218
- def include_in_body?(response, expected)
219
- response.body.include? expected
220
- end
221
-
222
- # Whether the request found the desired value or not
223
- def found?(response)
224
- status_code_for(response) != 404
225
- end
226
-
227
- # Convert XML or JSON response into a Hash
228
- # @param [String] response Response as a String (either in XML or JSON)
229
- # @return [Hash]
230
- def extract_hash(response)
231
- raise ArgumentError("Empty Body. Can't assert on it") if response.body.empty?
232
- case Interpreter.response_type_for response
233
- when :json
234
- converted = JSON.parse(response.body)
235
- return converted.transform_keys_to_symbols if converted.is_a? Hash
236
- return converted.map!(&:transform_keys_to_symbols) if converted.is_a? Array
237
- raise 'Incorrect Type prodcued ' + converted.class
238
- when :xml
239
- parser = Nori.new(convert_tags_to: lambda { |tag| tag.snakecase.to_sym })
240
- parser.parse(response.body)
241
- else
242
- raise "Neither XML nor JSON detected. It is #{type}. Don't know how to parse It is #{response.body}"
243
- end
244
- end
245
-
246
- # @return [Boolean] Whether response contains expected value
247
- def include_value?(response, expected)
248
- extract_hash(response).include_value? expected
249
- end
250
-
251
- # @return [Boolean] Whether response body contains expected key
252
- def include_key?(response, expected)
253
- value_from_path(response, expected)
254
- end
255
-
256
- # @return [Integer] HTTP Status code for response
257
- def status_code_for(response)
258
- response.code
259
- end
260
-
261
- # Override this to specify elements that must be present in the response
262
- # Will be used in 'success_scenarios' shared examples
263
- # @return [Array] Array of symbols specifying element names
264
- def mandatory_elements
265
- []
266
- end
267
-
268
- # Override this to specify xpath results that must be present in the response
269
- # Will be used in 'success_scenarios' shared examples
270
- # @return [Hash] Hash of 'xpath' => 'expected value' pairs
271
- def mandatory_xpath_values
272
- {}
273
- end
274
-
275
- # Attributes set at the root XML element of SOAP request
276
- def root_attributes
277
- nil
278
- end
279
-
280
- # Returns the value at the provided xpath
281
- # @param [RestClient::Response] response
282
- # @param [String] xpath
283
- # @return [Enumerable] Value inside element found through Xpath
284
- def xpath_elements_for(response: nil, xpath: nil, attribute: nil)
285
- raise ArgumentError unless response && xpath
286
- raise "Can't perform XPATH if response is not XML" unless Interpreter.response_type_for(response) == :xml
287
- xpath = "//*[@#{attribute}]" unless attribute.nil?
288
- if xpath[0] != '/'
289
- xpath = convert_to_pascal_case(xpath) if pascal_keys?
290
- xpath = '//' + xpath
291
- end
292
- if Soaspec.strip_namespaces? && !xpath.include?(':')
293
- temp_doc = Nokogiri.parse response.body
294
- temp_doc.remove_namespaces!
295
- temp_doc.xpath(xpath)
296
- else
297
- Nokogiri.parse(response.body).xpath(xpath)
298
- end
299
- end
300
-
301
- # @return [Enumerable] List of values matching JSON path
302
- def json_path_values_for(response, path, attribute: nil)
303
- raise 'JSON does not support attributes' if attribute
304
- if path[0] != '$'
305
- path = convert_to_pascal_case(path) if pascal_keys?
306
- path = '$..' + path
307
- end
308
- JsonPath.on(response.body, path)
309
- end
310
-
311
- # Based on a exchange, return the value at the provided xpath
312
- # If the path does not begin with a '/', a '//' is added to it
313
- # @param [Response] response
314
- # @param [Object] path Xpath, JSONPath or other path identifying how to find element
315
- # @param [String] attribute Generic attribute to find. Will override path
316
- # @return [String] Value at Xpath
317
- def value_from_path(response, path, attribute: nil)
318
- path = path.to_s
319
- case Interpreter.response_type_for(response)
320
- when :xml
321
- result = xpath_elements_for(response: response, xpath: path, attribute: attribute).first
322
- raise NoElementAtPath, "No value at Xpath '#{path}'" unless result
323
- return result.inner_text if attribute.nil?
324
- return result.attributes[attribute].inner_text
325
- when :json
326
- matching_values = json_path_values_for(response, path, attribute: attribute)
327
- raise NoElementAtPath, "Element in #{response.body} not found with path '#{path}'" if matching_values.empty?
328
- matching_values.first
329
- when :hash
330
- response.dig(path.split('.')) # Use path as Hash dig expression separating params via '.' TODO: Unit test
331
- else
332
- response.to_s[/path/] # Perform regular expression using path if not XML nor JSON TODO: Unit test
333
- end
334
- end
335
-
336
- # @return [Enumerable] List of values returned from path
337
- def values_from_path(response, path, attribute: nil)
338
- path = path.to_s
339
- case Interpreter.response_type_for(response)
340
- when :xml
341
- xpath_elements_for(response: response, xpath: path, attribute: attribute).map(&:inner_text)
342
- when :json
343
- json_path_values_for(response, path, attribute: attribute)
344
- else
345
- raise "Unable to interpret type of #{response.body}"
346
- end
347
- end
348
-
349
- # Convenience methods for once off usage of a REST request
350
- class << self
351
-
352
- methods = %w[post patch put get delete]
353
-
354
- methods.each do |rest_method|
355
- # Make REST Exchange within this Handler context
356
- # @param [Hash] params Exchange parameters
357
- # @return [Exchange] Instance of Exchange class. Assertions are made by default on the response body
358
- define_method(rest_method) do |params|
359
- params ||= {}
360
- params[:name] ||= rest_method
361
- new(params[:name])
362
- Exchange.new(params[:name], method: rest_method.to_sym, **params)
363
- end
364
- end
365
- end
366
-
367
- end
1
+
2
+ require_relative 'exchange_handler'
3
+ require_relative '../core_ext/hash'
4
+ require_relative '../not_found_errors'
5
+ require_relative 'handler_accessors'
6
+ require_relative '../interpreter'
7
+ require 'json'
8
+ require 'jsonpath'
9
+ require 'nori'
10
+ require 'erb'
11
+
12
+ module Soaspec
13
+
14
+ # Accessors specific to REST handler
15
+ module RestAccessors
16
+
17
+ # Defines method 'base_url_value' containing base URL used in REST requests
18
+ # @param [String] url Base Url to use in REST requests. Suburl is appended to this
19
+ def base_url(url)
20
+ define_method('base_url_value') do
21
+ url
22
+ end
23
+ end
24
+
25
+ # Will create access_token method based on passed parameters
26
+ def oauth2(client_id: nil, client_secret: nil, token_url: nil, username: nil, password: nil, security_token: nil)
27
+ define_method('oauth_response') do
28
+ username = api_username || ERB.new(username).result(binding) if username
29
+ security_token = ERB.new(security_token).result(binding) if security_token
30
+ token_url = ERB.new(token_url).result(binding) if token_url
31
+ password = ERB.new(password).result(binding) if password
32
+ payload = if password && username
33
+ {
34
+ grant_type: 'password',
35
+ client_id: client_id,
36
+ client_secret: client_secret,
37
+ username: username,
38
+ password: security_token ? (password + security_token) : password,
39
+ multipart: true
40
+ }
41
+ else
42
+ {
43
+ grant_type: 'client_credentials',
44
+ client_id: client_id,
45
+ client_secret: client_secret
46
+ }
47
+ end
48
+ retry_count = 0
49
+ begin
50
+ Soaspec::SpecLogger.add_to 'request_params: ' + payload.to_s
51
+ response = RestClient.post(token_url, payload, cache_control: 'no_cache', verify_ssl: false)
52
+ rescue RestClient::Exception => e
53
+ Soaspec::SpecLogger.add_to("oauth_error: #{e.message}")
54
+ Soaspec::SpecLogger.add_to("oauth_error: #{e.response}")
55
+ retry_count += 1
56
+ retry if retry_count < 3
57
+ raise e
58
+ end
59
+ Soaspec::SpecLogger.add_to("response_headers: #{response.headers}")
60
+ Soaspec::SpecLogger.add_to("response_body: #{response.body}")
61
+ JSON.parse(response)
62
+ end
63
+
64
+ define_method('access_token') do
65
+ oauth_response['access_token']
66
+ end
67
+ define_method('instance_url') do
68
+ oauth_response['instance_url']
69
+ end
70
+ end
71
+
72
+ # Pass path to YAML file containing OAuth2 parameters
73
+ # @param [String] path_to_filename Will have Soaspec.credentials_folder appended to it if set
74
+ def oauth2_file(path_to_filename)
75
+ full_path = Soaspec.credentials_folder ? File.join(Soaspec.credentials_folder, path_to_filename + '.yml') : path_to_filename + '.yml'
76
+ file_hash = YAML.load_file(full_path)
77
+ raise 'File at ' + full_path + ' is not a hash ' unless file_hash.is_a? Hash
78
+ oauth_hash = file_hash.transform_keys_to_symbols
79
+ oauth2 **oauth_hash
80
+ end
81
+
82
+ # @param [Hash] headers Hash of REST headers used in RestClient
83
+ def headers(headers)
84
+ define_method('rest_client_headers') do
85
+ headers
86
+ end
87
+ end
88
+
89
+ # Convert each key from snake_case to PascalCase
90
+ def pascal_keys(set)
91
+ define_method('pascal_keys?') do
92
+ set
93
+ end
94
+ end
95
+
96
+ end
97
+
98
+ # Wraps around Savon client defining default values dependent on the soap request
99
+ class RestHandler < ExchangeHandler
100
+ extend Soaspec::RestAccessors
101
+
102
+ # User used in making API calls
103
+ attr_accessor :api_username
104
+
105
+ # Set through following method. Base URL in REST requests.
106
+ def base_url_value
107
+ nil
108
+ end
109
+
110
+ # Headers used in RestClient
111
+ def rest_client_headers
112
+ {}
113
+ end
114
+
115
+ # Add values to here when extending this class to have default REST options.
116
+ # See rest client resource at https://github.com/rest-client/rest-client for details
117
+ # It's easier to set headers via 'headers' accessor rather than here
118
+ # @return [Hash] Options adding to & overriding defaults
119
+ def rest_resource_options
120
+ {
121
+ }
122
+ end
123
+
124
+ # Perform ERB on each header value
125
+ # @return [Hash] Hash from 'rest_client_headers' passed through ERB
126
+ def parse_headers
127
+ Hash[rest_client_headers.map { |k, header| [k, ERB.new(header).result(binding)] }]
128
+ end
129
+
130
+ # Setup object to handle communicating with a particular SOAP WSDL
131
+ # @param [Hash] options Options defining SOAP request. WSDL, authentication
132
+ def initialize(name = self.class.to_s, options = {})
133
+ raise "Base URL not set! Please set in class with 'base_url' method" unless base_url_value
134
+ @default_hash = {}
135
+ if name.is_a?(Hash) && options == {} # If name is not set
136
+ options = name
137
+ name = self.class.to_s
138
+ end
139
+ super
140
+ set_remove_key(options, :api_username)
141
+ set_remove_key(options, :default_hash)
142
+ @init_options = options
143
+ end
144
+
145
+ # Convert snakecase to PascalCase
146
+ def convert_to_pascal_case(key)
147
+ return key if /[[:upper:]]/ =~ key[0] # If first character already capital, don't do conversion
148
+ key.split('_').map(&:capitalize).join
149
+ end
150
+
151
+ # Whether to convert each key in the request to PascalCase
152
+ # It will also auto convert simple XPath, JSONPath where '//' or '..' not specified
153
+ # @return Whether to convert to PascalCase
154
+ def pascal_keys?
155
+ false
156
+ end
157
+
158
+ # @return [Hash]
159
+ def hash_used_in_request(override_hash)
160
+ request = @default_hash.merge(override_hash)
161
+ if pascal_keys?
162
+ request.map { |k, v| [convert_to_pascal_case(k.to_s), v] }.to_h
163
+ else
164
+ request
165
+ end
166
+ end
167
+
168
+ # Initialize value of merged options
169
+ def init_merge_options
170
+ options = rest_resource_options
171
+ options[:headers] ||= {}
172
+ options[:headers].merge! parse_headers
173
+ options.merge(@init_options)
174
+ end
175
+
176
+ # Used in together with Exchange request that passes such override parameters
177
+ # @param [Hash] override_parameters Params to characterize REST request
178
+ # @param_value [params] Extra parameters (E.g. headers)
179
+ # @param_value [suburl] URL appended to base_url of class
180
+ # @param_value [method] REST method (:get, :post, etc)
181
+ def make_request(override_parameters)
182
+ @merged_options ||= init_merge_options
183
+ test_values = override_parameters
184
+ test_values[:params] ||= {}
185
+ test_values[:method] ||= :post
186
+ test_values[:suburl] = test_values[:suburl].to_s if test_values[:suburl]
187
+ test_values[:params][:params] = test_values[:q] if test_values[:q] # Use q for query parameters. Nested :params is ugly and long
188
+
189
+ # In order for ERB to be calculated at correct time, the first time request is made, the resource should be created
190
+ @resource ||= RestClient::Resource.new(ERB.new(base_url_value).result(binding), @merged_options)
191
+
192
+ @resource_used = test_values[:suburl] ? @resource[test_values[:suburl]] : @resource
193
+
194
+ begin
195
+ response = case test_values[:method]
196
+ when :post, :patch, :put
197
+ unless test_values[:payload]
198
+ test_values[:payload] = JSON.generate(hash_used_in_request(test_values[:body])).to_s if test_values[:body]
199
+ end
200
+ @resource_used.send(test_values[:method].to_s, test_values[:payload], test_values[:params])
201
+ else
202
+ @resource_used.send(test_values[:method].to_s, test_values[:params])
203
+ end
204
+ rescue RestClient::ExceptionWithResponse => e
205
+ response = e.response
206
+ end
207
+ Soaspec::SpecLogger.add_to('response_headers: ' + response.headers.to_s)
208
+ Soaspec::SpecLogger.add_to('response_body: ' + response.to_s)
209
+ response
210
+ end
211
+
212
+ # @param [Hash] _format Format of expected result. Ignored for this
213
+ # @return [Object] Generic body to be displayed in error messages
214
+ def response_body(response, _format: :hash)
215
+ extract_hash response
216
+ end
217
+
218
+ def include_in_body?(response, expected)
219
+ response.body.include? expected
220
+ end
221
+
222
+ # Whether the request found the desired value or not
223
+ def found?(response)
224
+ status_code_for(response) != 404
225
+ end
226
+
227
+ # Convert XML or JSON response into a Hash
228
+ # @param [String] response Response as a String (either in XML or JSON)
229
+ # @return [Hash]
230
+ def extract_hash(response)
231
+ raise ArgumentError("Empty Body. Can't assert on it") if response.body.empty?
232
+ case Interpreter.response_type_for response
233
+ when :json
234
+ converted = JSON.parse(response.body)
235
+ return converted.transform_keys_to_symbols if converted.is_a? Hash
236
+ return converted.map!(&:transform_keys_to_symbols) if converted.is_a? Array
237
+ raise 'Incorrect Type prodcued ' + converted.class
238
+ when :xml
239
+ parser = Nori.new(convert_tags_to: lambda { |tag| tag.snakecase.to_sym })
240
+ parser.parse(response.body)
241
+ else
242
+ raise "Neither XML nor JSON detected. It is #{type}. Don't know how to parse It is #{response.body}"
243
+ end
244
+ end
245
+
246
+ # @return [Boolean] Whether response contains expected value
247
+ def include_value?(response, expected)
248
+ extract_hash(response).include_value? expected
249
+ end
250
+
251
+ # @return [Boolean] Whether response body contains expected key
252
+ def include_key?(response, expected)
253
+ value_from_path(response, expected)
254
+ end
255
+
256
+ # @return [Integer] HTTP Status code for response
257
+ def status_code_for(response)
258
+ response.code
259
+ end
260
+
261
+ # Override this to specify elements that must be present in the response
262
+ # Will be used in 'success_scenarios' shared examples
263
+ # @return [Array] Array of symbols specifying element names
264
+ def mandatory_elements
265
+ []
266
+ end
267
+
268
+ # Override this to specify xpath results that must be present in the response
269
+ # Will be used in 'success_scenarios' shared examples
270
+ # @return [Hash] Hash of 'xpath' => 'expected value' pairs
271
+ def mandatory_xpath_values
272
+ {}
273
+ end
274
+
275
+ # Attributes set at the root XML element of SOAP request
276
+ def root_attributes
277
+ nil
278
+ end
279
+
280
+ # Returns the value at the provided xpath
281
+ # @param [RestClient::Response] response
282
+ # @param [String] xpath
283
+ # @return [Enumerable] Value inside element found through Xpath
284
+ def xpath_elements_for(response: nil, xpath: nil, attribute: nil)
285
+ raise ArgumentError unless response && xpath
286
+ raise "Can't perform XPATH if response is not XML" unless Interpreter.response_type_for(response) == :xml
287
+ xpath = "//*[@#{attribute}]" unless attribute.nil?
288
+ if xpath[0] != '/'
289
+ xpath = convert_to_pascal_case(xpath) if pascal_keys?
290
+ xpath = '//' + xpath
291
+ end
292
+ temp_doc = Nokogiri.parse(response.body).dup
293
+ if Soaspec.strip_namespaces? && !xpath.include?(':')
294
+ temp_doc.remove_namespaces!
295
+ temp_doc.xpath(xpath)
296
+ else
297
+ temp_doc.xpath(xpath, temp_doc.collect_namespaces)
298
+ end
299
+ end
300
+
301
+ # @return [Enumerable] List of values matching JSON path
302
+ def json_path_values_for(response, path, attribute: nil)
303
+ raise 'JSON does not support attributes' if attribute
304
+ if path[0] != '$'
305
+ path = convert_to_pascal_case(path) if pascal_keys?
306
+ path = '$..' + path
307
+ end
308
+ JsonPath.on(response.body, path)
309
+ end
310
+
311
+ # Based on a exchange, return the value at the provided xpath
312
+ # If the path does not begin with a '/', a '//' is added to it
313
+ # @param [Response] response
314
+ # @param [Object] path Xpath, JSONPath or other path identifying how to find element
315
+ # @param [String] attribute Generic attribute to find. Will override path
316
+ # @return [String] Value at Xpath
317
+ def value_from_path(response, path, attribute: nil)
318
+ path = path.to_s
319
+ case Interpreter.response_type_for(response)
320
+ when :xml
321
+ result = xpath_elements_for(response: response, xpath: path, attribute: attribute).first
322
+ raise NoElementAtPath, "No value at Xpath '#{path}'" unless result
323
+ return result.inner_text if attribute.nil?
324
+ return result.attributes[attribute].inner_text
325
+ when :json
326
+ matching_values = json_path_values_for(response, path, attribute: attribute)
327
+ raise NoElementAtPath, "Element in #{response.body} not found with path '#{path}'" if matching_values.empty?
328
+ matching_values.first
329
+ when :hash
330
+ response.dig(path.split('.')) # Use path as Hash dig expression separating params via '.' TODO: Unit test
331
+ else
332
+ response.to_s[/path/] # Perform regular expression using path if not XML nor JSON TODO: Unit test
333
+ end
334
+ end
335
+
336
+ # @return [Enumerable] List of values returned from path
337
+ def values_from_path(response, path, attribute: nil)
338
+ path = path.to_s
339
+ case Interpreter.response_type_for(response)
340
+ when :xml
341
+ xpath_elements_for(response: response, xpath: path, attribute: attribute).map(&:inner_text)
342
+ when :json
343
+ json_path_values_for(response, path, attribute: attribute)
344
+ else
345
+ raise "Unable to interpret type of #{response.body}"
346
+ end
347
+ end
348
+
349
+ # Convenience methods for once off usage of a REST request
350
+ class << self
351
+
352
+ methods = %w[post patch put get delete]
353
+
354
+ methods.each do |rest_method|
355
+ # Make REST Exchange within this Handler context
356
+ # @param [Hash] params Exchange parameters
357
+ # @return [Exchange] Instance of Exchange class. Assertions are made by default on the response body
358
+ define_method(rest_method) do |params|
359
+ params ||= {}
360
+ params[:name] ||= rest_method
361
+ new(params[:name])
362
+ Exchange.new(params[:name], method: rest_method.to_sym, **params)
363
+ end
364
+ end
365
+ end
366
+
367
+ end
368
368
  end