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.
- checksums.yaml +5 -5
- data/.gitignore +15 -15
- data/.gitlab-ci.yml +31 -31
- data/.rspec +3 -3
- data/.rubocop.yml +2 -2
- data/.travis.yml +5 -5
- data/CODE_OF_CONDUCT.md +74 -74
- data/ChangeLog +345 -341
- data/Gemfile +6 -6
- data/LICENSE.txt +21 -21
- data/README.md +85 -85
- data/Rakefile +24 -24
- data/Todo.md +6 -6
- data/exe/soaspec +123 -123
- data/exe/soaspec-virtual-server +98 -92
- data/exe/xml_to_yaml_file +60 -60
- data/lib/soaspec.rb +86 -85
- data/lib/soaspec/core_ext/hash.rb +83 -83
- data/lib/soaspec/exchange.rb +230 -230
- data/lib/soaspec/exchange_handlers/exchange_handler.rb +98 -98
- data/lib/soaspec/exchange_handlers/handler_accessors.rb +95 -95
- data/lib/soaspec/exchange_handlers/rest_handler.rb +367 -367
- data/lib/soaspec/exchange_handlers/rest_methods.rb +44 -44
- data/lib/soaspec/exchange_handlers/soap_handler.rb +231 -231
- data/lib/soaspec/exe_helpers.rb +56 -56
- data/lib/soaspec/generator/.rspec.erb +5 -5
- data/lib/soaspec/generator/.travis.yml.erb +5 -5
- data/lib/soaspec/generator/Gemfile.erb +8 -8
- data/lib/soaspec/generator/README.md.erb +29 -29
- data/lib/soaspec/generator/Rakefile.erb +19 -19
- data/lib/soaspec/generator/config/data/default.yml.erb +1 -1
- data/lib/soaspec/generator/lib/blz_service.rb.erb +24 -24
- data/lib/soaspec/generator/lib/dynamic_class_content.rb.erb +12 -12
- data/lib/soaspec/generator/lib/shared_example.rb.erb +8 -8
- data/lib/soaspec/generator/spec/dynamic_soap_spec.rb.erb +14 -14
- data/lib/soaspec/generator/spec/soap_spec.rb.erb +53 -53
- data/lib/soaspec/generator/spec/spec_helper.rb.erb +20 -20
- data/lib/soaspec/generator/template/soap_template.xml +6 -6
- data/lib/soaspec/interpreter.rb +25 -25
- data/lib/soaspec/matchers.rb +65 -65
- data/lib/soaspec/not_found_errors.rb +13 -13
- data/lib/soaspec/soaspec_shared_examples.rb +24 -24
- data/lib/soaspec/spec_logger.rb +27 -27
- data/lib/soaspec/test_server/bank.wsdl +90 -90
- data/lib/soaspec/test_server/get_bank.rb +160 -160
- data/lib/soaspec/test_server/invoices.rb +27 -27
- data/lib/soaspec/test_server/namespace.xml +15 -0
- data/lib/soaspec/test_server/note.xml +5 -5
- data/lib/soaspec/test_server/puppy_service.rb +20 -20
- data/lib/soaspec/test_server/test_attribute.rb +13 -13
- data/lib/soaspec/test_server/test_namespace.rb +13 -0
- data/lib/soaspec/version.rb +2 -2
- data/lib/soaspec/wsdl_generator.rb +93 -93
- data/soaspec.gemspec +45 -45
- data/test.wsdl +116 -116
- data/test.xml +10 -10
- data/test_rest.rb +97 -97
- data/test_wsdl.rb +43 -43
- 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
|
-
|
293
|
-
|
294
|
-
temp_doc.remove_namespaces!
|
295
|
-
temp_doc.xpath(xpath)
|
296
|
-
else
|
297
|
-
|
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
|