savon_with_adapter 2.4.1

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 (87) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +14 -0
  3. data/.travis.yml +11 -0
  4. data/.yardopts +6 -0
  5. data/CHANGELOG.md +1042 -0
  6. data/CONTRIBUTING.md +46 -0
  7. data/Gemfile +18 -0
  8. data/LICENSE +20 -0
  9. data/README.md +81 -0
  10. data/Rakefile +14 -0
  11. data/donate.png +0 -0
  12. data/lib/savon.rb +27 -0
  13. data/lib/savon/block_interface.rb +26 -0
  14. data/lib/savon/builder.rb +166 -0
  15. data/lib/savon/client.rb +89 -0
  16. data/lib/savon/core_ext/string.rb +29 -0
  17. data/lib/savon/header.rb +70 -0
  18. data/lib/savon/http_error.rb +27 -0
  19. data/lib/savon/log_message.rb +48 -0
  20. data/lib/savon/message.rb +35 -0
  21. data/lib/savon/mock.rb +5 -0
  22. data/lib/savon/mock/expectation.rb +71 -0
  23. data/lib/savon/mock/spec_helper.rb +62 -0
  24. data/lib/savon/model.rb +80 -0
  25. data/lib/savon/operation.rb +127 -0
  26. data/lib/savon/options.rb +336 -0
  27. data/lib/savon/qualified_message.rb +49 -0
  28. data/lib/savon/request.rb +89 -0
  29. data/lib/savon/request_logger.rb +48 -0
  30. data/lib/savon/response.rb +112 -0
  31. data/lib/savon/soap_fault.rb +48 -0
  32. data/lib/savon/version.rb +3 -0
  33. data/savon.gemspec +52 -0
  34. data/spec/fixtures/gzip/message.gz +0 -0
  35. data/spec/fixtures/response/another_soap_fault.xml +14 -0
  36. data/spec/fixtures/response/authentication.xml +14 -0
  37. data/spec/fixtures/response/header.xml +13 -0
  38. data/spec/fixtures/response/list.xml +18 -0
  39. data/spec/fixtures/response/multi_ref.xml +39 -0
  40. data/spec/fixtures/response/soap_fault.xml +8 -0
  41. data/spec/fixtures/response/soap_fault12.xml +18 -0
  42. data/spec/fixtures/response/taxcloud.xml +1 -0
  43. data/spec/fixtures/ssl/client_cert.pem +16 -0
  44. data/spec/fixtures/ssl/client_encrypted_key.pem +30 -0
  45. data/spec/fixtures/ssl/client_encrypted_key_cert.pem +24 -0
  46. data/spec/fixtures/ssl/client_key.pem +15 -0
  47. data/spec/fixtures/wsdl/authentication.xml +63 -0
  48. data/spec/fixtures/wsdl/betfair.xml +2981 -0
  49. data/spec/fixtures/wsdl/edialog.xml +15416 -0
  50. data/spec/fixtures/wsdl/interhome.xml +2137 -0
  51. data/spec/fixtures/wsdl/lower_camel.xml +52 -0
  52. data/spec/fixtures/wsdl/multiple_namespaces.xml +92 -0
  53. data/spec/fixtures/wsdl/multiple_types.xml +60 -0
  54. data/spec/fixtures/wsdl/taxcloud.xml +934 -0
  55. data/spec/fixtures/wsdl/team_software.xml +1 -0
  56. data/spec/fixtures/wsdl/vies.xml +176 -0
  57. data/spec/fixtures/wsdl/wasmuth.xml +153 -0
  58. data/spec/integration/centra_spec.rb +72 -0
  59. data/spec/integration/email_example_spec.rb +32 -0
  60. data/spec/integration/random_quote_spec.rb +23 -0
  61. data/spec/integration/ratp_example_spec.rb +28 -0
  62. data/spec/integration/stockquote_example_spec.rb +28 -0
  63. data/spec/integration/support/application.rb +82 -0
  64. data/spec/integration/support/server.rb +84 -0
  65. data/spec/integration/temperature_example_spec.rb +46 -0
  66. data/spec/integration/zipcode_example_spec.rb +42 -0
  67. data/spec/savon/builder_spec.rb +86 -0
  68. data/spec/savon/client_spec.rb +198 -0
  69. data/spec/savon/core_ext/string_spec.rb +37 -0
  70. data/spec/savon/features/message_tag_spec.rb +61 -0
  71. data/spec/savon/http_error_spec.rb +49 -0
  72. data/spec/savon/log_message_spec.rb +33 -0
  73. data/spec/savon/message_spec.rb +40 -0
  74. data/spec/savon/mock_spec.rb +157 -0
  75. data/spec/savon/model_spec.rb +154 -0
  76. data/spec/savon/observers_spec.rb +92 -0
  77. data/spec/savon/operation_spec.rb +211 -0
  78. data/spec/savon/options_spec.rb +772 -0
  79. data/spec/savon/request_spec.rb +493 -0
  80. data/spec/savon/response_spec.rb +258 -0
  81. data/spec/savon/soap_fault_spec.rb +126 -0
  82. data/spec/spec_helper.rb +30 -0
  83. data/spec/support/endpoint.rb +25 -0
  84. data/spec/support/fixture.rb +39 -0
  85. data/spec/support/integration.rb +9 -0
  86. data/spec/support/stdout.rb +25 -0
  87. metadata +310 -0
@@ -0,0 +1,92 @@
1
+ require "spec_helper"
2
+ require "integration/support/server"
3
+
4
+ describe Savon do
5
+
6
+ before :all do
7
+ @server = IntegrationServer.run
8
+ end
9
+
10
+ after :all do
11
+ @server.stop
12
+ end
13
+
14
+ describe ".observers" do
15
+ after :each do
16
+ Savon.observers.clear
17
+ end
18
+
19
+ it "allows to register an observer for every request" do
20
+ observer = Class.new {
21
+
22
+ def notify(operation_name, builder, globals, locals)
23
+ @operation_name = operation_name
24
+
25
+ @builder = builder
26
+ @globals = globals
27
+ @locals = locals
28
+
29
+ # return nil to execute the request
30
+ nil
31
+ end
32
+
33
+ attr_reader :operation_name, :builder, :globals, :locals
34
+
35
+ }.new
36
+
37
+ Savon.observers << observer
38
+
39
+ new_client.call(:authenticate)
40
+
41
+ expect(observer.operation_name).to eq(:authenticate)
42
+
43
+ expect(observer.builder).to be_a(Savon::Builder)
44
+ expect(observer.globals).to be_a(Savon::GlobalOptions)
45
+ expect(observer.locals).to be_a(Savon::LocalOptions)
46
+ end
47
+
48
+ it "allows to register an observer which mocks requests" do
49
+ observer = Class.new {
50
+
51
+ def notify(*)
52
+ # return a response to mock the request
53
+ HTTPI::Response.new(201, { "X-Result" => "valid" }, "valid!")
54
+ end
55
+
56
+ }.new
57
+
58
+ Savon.observers << observer
59
+
60
+ response = new_client.call(:authenticate)
61
+
62
+ expect(response.http.code).to eq(201)
63
+ expect(response.http.headers).to eq("X-Result" => "valid")
64
+ expect(response.http.body).to eq("valid!")
65
+ end
66
+
67
+ it "raises if an observer returns something other than nil or an HTTPI::Response" do
68
+ observer = Class.new {
69
+
70
+ def notify(*)
71
+ []
72
+ end
73
+
74
+ }.new
75
+
76
+ Savon.observers << observer
77
+
78
+ expect { new_client.call(:authenticate) }.
79
+ to raise_error(Savon::Error, "Observers need to return an HTTPI::Response " \
80
+ "to mock the request or nil to execute the request.")
81
+ end
82
+ end
83
+
84
+ def new_client
85
+ Savon.client(
86
+ :endpoint => @server.url(:repeat),
87
+ :namespace => "http://v1.example.com",
88
+ :log => false
89
+ )
90
+ end
91
+
92
+ end
@@ -0,0 +1,211 @@
1
+ require "spec_helper"
2
+ require "integration/support/server"
3
+ require "json"
4
+ require "ostruct"
5
+
6
+ describe Savon::Operation do
7
+
8
+ let(:globals) { Savon::GlobalOptions.new(:endpoint => @server.url(:repeat), :log => false) }
9
+ let(:wsdl) { Wasabi::Document.new Fixture.wsdl(:taxcloud) }
10
+
11
+ let(:no_wsdl) {
12
+ wsdl = Wasabi::Document.new
13
+
14
+ wsdl.endpoint = "http://example.com"
15
+ wsdl.namespace = "http://v1.example.com"
16
+
17
+ wsdl
18
+ }
19
+
20
+ def new_operation(operation_name, wsdl, globals)
21
+ Savon::Operation.create(operation_name, wsdl, globals)
22
+ end
23
+
24
+ before :all do
25
+ @server = IntegrationServer.run
26
+ end
27
+
28
+ after :all do
29
+ @server.stop
30
+ end
31
+
32
+ describe ".create with a WSDL" do
33
+ it "returns a new operation" do
34
+ operation = new_operation(:verify_address, wsdl, globals)
35
+ expect(operation).to be_a(Savon::Operation)
36
+ end
37
+
38
+ it "raises if the operation name is not a Symbol" do
39
+ expect { new_operation("not a symbol", wsdl, globals) }.
40
+ to raise_error(ArgumentError, /Expected the first parameter \(the name of the operation to call\) to be a symbol/)
41
+ end
42
+
43
+ it "raises if the operation is not available for the service" do
44
+ expect { new_operation(:no_such_operation, wsdl, globals) }.
45
+ to raise_error(Savon::UnknownOperationError, /Unable to find SOAP operation: :no_such_operation/)
46
+ end
47
+ end
48
+
49
+ describe ".create without a WSDL" do
50
+ it "returns a new operation" do
51
+ operation = new_operation(:verify_address, no_wsdl, globals)
52
+ expect(operation).to be_a(Savon::Operation)
53
+ end
54
+ end
55
+
56
+ describe "#build" do
57
+ it "returns the Builder" do
58
+ operation = new_operation(:verify_address, wsdl, globals)
59
+ builder = operation.build(:message => { :test => 'message' })
60
+
61
+ expect(builder).to be_a(Savon::Builder)
62
+ expect(builder.to_s).to include('<tns:VerifyAddress><tns:test>message</tns:test></tns:VerifyAddress>')
63
+ end
64
+ end
65
+
66
+ describe "#call" do
67
+ it "returns a response object" do
68
+ operation = new_operation(:verify_address, wsdl, globals)
69
+ expect(operation.call).to be_a(Savon::Response)
70
+ end
71
+
72
+ it "uses the global :endpoint option for the request" do
73
+ globals.endpoint("http://v1.example.com")
74
+ HTTPI::Request.any_instance.expects(:url=).with("http://v1.example.com")
75
+
76
+ operation = new_operation(:verify_address, wsdl, globals)
77
+
78
+ # stub the actual request
79
+ http_response = HTTPI::Response.new(200, {}, "")
80
+ operation.expects(:call_with_logging).returns(http_response)
81
+
82
+ operation.call
83
+ end
84
+
85
+ it "falls back to use the WSDL's endpoint if the :endpoint option was not set" do
86
+ globals_without_endpoint = Savon::GlobalOptions.new(:log => false)
87
+ HTTPI::Request.any_instance.expects(:url=).with(wsdl.endpoint)
88
+
89
+ operation = new_operation(:verify_address, wsdl, globals_without_endpoint)
90
+
91
+ # stub the actual request
92
+ http_response = HTTPI::Response.new(200, {}, "")
93
+ operation.expects(:call_with_logging).returns(http_response)
94
+
95
+ operation.call
96
+ end
97
+
98
+ it "sets the Content-Length header" do
99
+ # XXX: probably the worst spec ever written. refactor! [dh, 2013-01-05]
100
+ http_request = HTTPI::Request.new
101
+ http_request.headers.expects(:[]=).with("Content-Length", "312")
102
+ Savon::SOAPRequest.any_instance.expects(:build).returns(http_request)
103
+
104
+ new_operation(:verify_address, wsdl, globals).call
105
+ end
106
+
107
+ it "passes the local :soap_action option to the request builder" do
108
+ globals.endpoint @server.url(:inspect_request)
109
+ soap_action = "http://v1.example.com/VerifyAddress"
110
+
111
+ operation = new_operation(:verify_address, wsdl, globals)
112
+ response = operation.call(:soap_action => soap_action)
113
+
114
+ actual_soap_action = inspect_request(response).soap_action
115
+ expect(actual_soap_action).to eq(%("#{soap_action}"))
116
+ end
117
+
118
+ it "uses the local :cookies option" do
119
+ globals.endpoint @server.url(:inspect_request)
120
+ cookies = [HTTPI::Cookie.new("some-cookie=choc-chip")]
121
+
122
+ HTTPI::Request.any_instance.expects(:set_cookies).with(cookies)
123
+
124
+ operation = new_operation(:verify_address, wsdl, globals)
125
+ operation.call(:cookies => cookies)
126
+ end
127
+
128
+ it "passes nil to the request builder if the :soap_action was set to nil" do
129
+ globals.endpoint @server.url(:inspect_request)
130
+
131
+ operation = new_operation(:verify_address, wsdl, globals)
132
+ response = operation.call(:soap_action => nil)
133
+
134
+ actual_soap_action = inspect_request(response).soap_action
135
+ expect(actual_soap_action).to be_nil
136
+ end
137
+
138
+ it "gets the SOAP action from the WSDL if available" do
139
+ globals.endpoint @server.url(:inspect_request)
140
+
141
+ operation = new_operation(:verify_address, wsdl, globals)
142
+ response = operation.call
143
+
144
+ actual_soap_action = inspect_request(response).soap_action
145
+ expect(actual_soap_action).to eq('"http://taxcloud.net/VerifyAddress"')
146
+ end
147
+
148
+ it "falls back to Gyoku if both option and WSDL are not available" do
149
+ globals.endpoint @server.url(:inspect_request)
150
+
151
+ operation = new_operation(:authenticate, no_wsdl, globals)
152
+ response = operation.call
153
+
154
+ actual_soap_action = inspect_request(response).soap_action
155
+ expect(actual_soap_action).to eq(%("authenticate"))
156
+ end
157
+
158
+ it "returns a Savon::Multipart::Response if available and requested globally" do
159
+ globals.multipart true
160
+
161
+ with_multipart_mocked do
162
+ operation = new_operation(:authenticate, no_wsdl, globals)
163
+ response = operation.call
164
+
165
+ expect(response).to be_a(Savon::Multipart::Response)
166
+ end
167
+ end
168
+
169
+ it "returns a Savon::Multipart::Response if available and requested locally" do
170
+ with_multipart_mocked do
171
+ operation = new_operation(:authenticate, no_wsdl, globals)
172
+ response = operation.call(:multipart => true)
173
+
174
+ expect(response).to be_a(Savon::Multipart::Response)
175
+ end
176
+ end
177
+
178
+ it "raises if savon-multipart is not available and it was requested globally" do
179
+ globals.multipart true
180
+
181
+ operation = new_operation(:authenticate, no_wsdl, globals)
182
+
183
+ expect { operation.call }.
184
+ to raise_error RuntimeError, /Unable to find Savon::Multipart/
185
+ end
186
+
187
+ it "raises if savon-multipart is not available and it was requested locally" do
188
+ operation = new_operation(:authenticate, no_wsdl, globals)
189
+
190
+ expect { operation.call(:multipart => true) }.
191
+ to raise_error RuntimeError, /Unable to find Savon::Multipart/
192
+ end
193
+ end
194
+
195
+ def with_multipart_mocked
196
+ multipart_response = Class.new { def initialize(*args); end }
197
+ multipart_mock = Module.new
198
+ multipart_mock.const_set('Response', multipart_response)
199
+
200
+ Savon.const_set('Multipart', multipart_mock)
201
+
202
+ yield
203
+ ensure
204
+ Savon.send(:remove_const, :Multipart) if Savon.const_defined? :Multipart
205
+ end
206
+
207
+ def inspect_request(response)
208
+ hash = JSON.parse(response.http.body)
209
+ OpenStruct.new(hash)
210
+ end
211
+ end
@@ -0,0 +1,772 @@
1
+ require "spec_helper"
2
+ require "integration/support/server"
3
+ require "json"
4
+ require "ostruct"
5
+ require "logger"
6
+
7
+ describe "Options" do
8
+
9
+ before :all do
10
+ @server = IntegrationServer.run
11
+ end
12
+
13
+ after :all do
14
+ @server.stop
15
+ end
16
+
17
+ context "global: endpoint and namespace" do
18
+ it "sets the SOAP endpoint to use to allow requests without a WSDL document" do
19
+ client = new_client_without_wsdl(:endpoint => @server.url(:repeat), :namespace => "http://v1.example.com")
20
+ response = client.call(:authenticate)
21
+
22
+ # the default namespace identifier is :wsdl and contains the namespace option
23
+ expect(response.http.body).to include('xmlns:wsdl="http://v1.example.com"')
24
+
25
+ # the default namespace applies to the message tag
26
+ expect(response.http.body).to include('<wsdl:authenticate>')
27
+ end
28
+ end
29
+
30
+ context "global :namespace_identifier" do
31
+ it "changes the default namespace identifier" do
32
+ client = new_client(:endpoint => @server.url(:repeat), :namespace_identifier => :lol)
33
+ response = client.call(:authenticate)
34
+
35
+ expect(response.http.body).to include('xmlns:lol="http://v1_0.ws.auth.order.example.com/"')
36
+ expect(response.http.body).to include("<lol:authenticate></lol:authenticate>")
37
+ end
38
+
39
+ it "ignores namespace identifier if it is nil" do
40
+ client = new_client(:endpoint => @server.url(:repeat), :namespace_identifier => nil)
41
+ response = client.call(:authenticate, :message => {:user => 'foo'})
42
+
43
+ expect(response.http.body).to include('xmlns="http://v1_0.ws.auth.order.example.com/"')
44
+ expect(response.http.body).to include("<authenticate><user>foo</user></authenticate>")
45
+ end
46
+ end
47
+
48
+ context "global :namespaces" do
49
+ it "adds additional namespaces to the SOAP envelope" do
50
+ namespaces = { "xmlns:whatever" => "http://whatever.example.com" }
51
+ client = new_client(:endpoint => @server.url(:repeat), :namespaces => namespaces)
52
+ response = client.call(:authenticate)
53
+
54
+ expect(response.http.body).to include('xmlns:whatever="http://whatever.example.com"')
55
+ end
56
+ end
57
+
58
+ context "global :proxy" do
59
+ it "sets the proxy server to use" do
60
+ proxy_url = "http://example.com"
61
+ client = new_client(:endpoint => @server.url, :proxy => proxy_url)
62
+
63
+ # TODO: find a way to integration test this [dh, 2012-12-08]
64
+ HTTPI::Request.any_instance.expects(:proxy=).with(proxy_url)
65
+
66
+ response = client.call(:authenticate)
67
+ end
68
+ end
69
+
70
+ context "global :headers" do
71
+ it "sets the HTTP headers for the next request" do
72
+ client = new_client(:endpoint => @server.url(:inspect_request), :headers => { "X-Token" => "secret" })
73
+
74
+ response = client.call(:authenticate)
75
+ x_token = inspect_request(response).x_token
76
+
77
+ expect(x_token).to eq("secret")
78
+ end
79
+ end
80
+
81
+ context "global :open_timeout" do
82
+ it "makes the client timeout after n seconds" do
83
+ non_routable_ip = "http://192.0.2.0"
84
+ client = new_client(:endpoint => non_routable_ip, :open_timeout => 0.1)
85
+
86
+ expect { client.call(:authenticate) }.to raise_error { |error|
87
+ if error.kind_of? Errno::EHOSTUNREACH
88
+ warn "Warning: looks like your network may be down?!\n" +
89
+ "-> skipping spec at #{__FILE__}:#{__LINE__}"
90
+ else
91
+ # TODO: make HTTPI tag timeout errors, then depend on HTTPI::TimeoutError
92
+ # instead of a specific client error [dh, 2012-12-08]
93
+ expect(error).to be_an(HTTPClient::ConnectTimeoutError)
94
+ end
95
+ }
96
+ end
97
+ end
98
+
99
+ context "global :read_timeout" do
100
+ it "makes the client timeout after n seconds" do
101
+ client = new_client(:endpoint => @server.url(:timeout), :open_timeout => 0.1, :read_timeout => 0.1)
102
+
103
+ expect { client.call(:authenticate) }.
104
+ to raise_error(HTTPClient::ReceiveTimeoutError)
105
+ end
106
+ end
107
+
108
+ context "global :encoding" do
109
+ it "changes the XML instruction" do
110
+ client = new_client(:endpoint => @server.url(:repeat), :encoding => "ISO-8859-1")
111
+ response = client.call(:authenticate)
112
+
113
+ expect(response.http.body).to match(/<\?xml version="1\.0" encoding="ISO-8859-1"\?>/)
114
+ end
115
+
116
+ it "changes the Content-Type header" do
117
+ client = new_client(:endpoint => @server.url(:inspect_request), :encoding => "ISO-8859-1")
118
+
119
+ response = client.call(:authenticate)
120
+ content_type = inspect_request(response).content_type
121
+ expect(content_type).to eq("text/xml;charset=ISO-8859-1")
122
+ end
123
+ end
124
+
125
+ context "global :soap_header" do
126
+ it "accepts a Hash of SOAP header information" do
127
+ client = new_client(:endpoint => @server.url(:repeat), :soap_header => { :auth_token => "secret" })
128
+ response = client.call(:authenticate)
129
+
130
+ expect(response.http.body).to include("<env:Header><authToken>secret</authToken></env:Header>")
131
+ end
132
+
133
+ it "accepts anything other than a String and calls #to_s on it" do
134
+ to_s_header = Class.new {
135
+ def to_s
136
+ "to_s_header"
137
+ end
138
+ }.new
139
+
140
+ client = new_client(:endpoint => @server.url(:repeat), :soap_header => to_s_header)
141
+ response = client.call(:authenticate)
142
+
143
+ expect(response.http.body).to include("<env:Header>to_s_header</env:Header>")
144
+ end
145
+ end
146
+
147
+ context "global :element_form_default" do
148
+ it "specifies whether elements should be :qualified or :unqualified" do
149
+ # qualified
150
+ client = new_client(:endpoint => @server.url(:repeat), :element_form_default => :qualified)
151
+
152
+ response = client.call(:authenticate, :message => { :user => "luke", :password => "secret" })
153
+ expect(response.http.body).to include("<tns:user>luke</tns:user>")
154
+ expect(response.http.body).to include("<tns:password>secret</tns:password>")
155
+
156
+ # unqualified
157
+ client = new_client(:endpoint => @server.url(:repeat), :element_form_default => :unqualified)
158
+
159
+ response = client.call(:authenticate, :message => { :user => "lea", :password => "top-secret" })
160
+ expect(response.http.body).to include("<user>lea</user>")
161
+ expect(response.http.body).to include("<password>top-secret</password>")
162
+ end
163
+ end
164
+
165
+ context "global :env_namespace" do
166
+ it "when set, replaces the default namespace identifier for the SOAP envelope" do
167
+ client = new_client(:endpoint => @server.url(:repeat), :env_namespace => "soapenv")
168
+ response = client.call(:authenticate)
169
+
170
+ expect(response.http.body).to include("<soapenv:Envelope")
171
+ end
172
+
173
+ it "when not set, Savon defaults to use :env as the namespace identifier for the SOAP envelope" do
174
+ client = new_client(:endpoint => @server.url(:repeat))
175
+ response = client.call(:authenticate)
176
+
177
+ expect(response.http.body).to include("<env:Envelope")
178
+ end
179
+ end
180
+
181
+ context "global :soap_version" do
182
+ it "it uses the correct SOAP 1.1 namespace" do
183
+ client = new_client(:endpoint => @server.url(:repeat), :soap_version => 1)
184
+ response = client.call(:authenticate)
185
+
186
+ expect(response.http.body).to include('xmlns:env="http://schemas.xmlsoap.org/soap/envelope/"')
187
+ end
188
+
189
+ it "it uses the correct SOAP 1.2 namespace" do
190
+ client = new_client(:endpoint => @server.url(:repeat), :soap_version => 2)
191
+ response = client.call(:authenticate)
192
+
193
+ expect(response.http.body).to include('xmlns:env="http://www.w3.org/2003/05/soap-envelope"')
194
+ end
195
+ end
196
+
197
+ context "global: raise_errors" do
198
+ it "when true, instructs Savon to raise SOAP fault errors" do
199
+ client = new_client(:endpoint => @server.url(:repeat), :raise_errors => true)
200
+
201
+ expect { client.call(:authenticate, :xml => Fixture.response(:soap_fault)) }.
202
+ to raise_error(Savon::SOAPFault)
203
+
204
+ begin
205
+ client.call(:authenticate, :xml => Fixture.response(:soap_fault))
206
+ rescue Savon::SOAPFault => soap_fault
207
+ # check whether the configured nori instance is used by the soap fault
208
+ expect(soap_fault.to_hash[:fault][:faultcode]).to eq("soap:Server")
209
+ end
210
+ end
211
+
212
+ it "when true, instructs Savon to raise HTTP errors" do
213
+ client = new_client(:endpoint => @server.url(404), :raise_errors => true)
214
+ expect { client.call(:authenticate) }.to raise_error(Savon::HTTPError)
215
+ end
216
+
217
+ it "when false, instructs Savon to not raise SOAP fault errors" do
218
+ client = new_client(:endpoint => @server.url(:repeat), :raise_errors => false)
219
+ response = client.call(:authenticate, :xml => Fixture.response(:soap_fault))
220
+
221
+ expect(response).to_not be_successful
222
+ expect(response).to be_a_soap_fault
223
+ end
224
+
225
+ it "when false, instructs Savon to not raise HTTP errors" do
226
+ client = new_client(:endpoint => @server.url(404), :raise_errors => false)
227
+ response = client.call(:authenticate)
228
+
229
+ expect(response).to_not be_successful
230
+ expect(response).to be_a_http_error
231
+ end
232
+ end
233
+
234
+ context "global :log" do
235
+ it "instructs Savon not to log SOAP requests and responses" do
236
+ stdout = mock_stdout {
237
+ client = new_client(:endpoint => @server.url, :log => false)
238
+ client.call(:authenticate)
239
+ }
240
+
241
+ expect(stdout.string).to be_empty
242
+ end
243
+
244
+ it "silences HTTPI as well" do
245
+ HTTPI.expects(:log=).with(false)
246
+ new_client(:log => false)
247
+ end
248
+
249
+ it "instructs Savon to log SOAP requests and responses" do
250
+ stdout = mock_stdout do
251
+ client = new_client(:endpoint => @server.url, :log => true)
252
+ client.call(:authenticate)
253
+ end
254
+
255
+ expect(stdout.string).to include("INFO -- : SOAP request")
256
+ end
257
+
258
+ it "turns HTTPI logging back on as well" do
259
+ HTTPI.expects(:log=).with(true)
260
+ new_client(:log => true)
261
+ end
262
+ end
263
+
264
+ context "global :logger" do
265
+ it "defaults to an instance of Ruby's standard Logger" do
266
+ logger = new_client.globals[:logger]
267
+ expect(logger).to be_a(Logger)
268
+ end
269
+
270
+ it "allows a custom logger to be set" do
271
+ custom_logger = Logger.new($stdout)
272
+
273
+ client = new_client(:logger => custom_logger, :log => true)
274
+ logger = client.globals[:logger]
275
+
276
+ expect(logger).to eq(custom_logger)
277
+ end
278
+ end
279
+
280
+ context "global :log_level" do
281
+ it "allows changing the Logger's log level to :debug" do
282
+ client = new_client(:log_level => :debug)
283
+ level = client.globals[:logger].level
284
+
285
+ expect(level).to eq(0)
286
+ end
287
+
288
+ it "allows changing the Logger's log level to :info" do
289
+ client = new_client(:log_level => :info)
290
+ level = client.globals[:logger].level
291
+
292
+ expect(level).to eq(1)
293
+ end
294
+
295
+ it "allows changing the Logger's log level to :warn" do
296
+ client = new_client(:log_level => :warn)
297
+ level = client.globals[:logger].level
298
+
299
+ expect(level).to eq(2)
300
+ end
301
+
302
+ it "allows changing the Logger's log level to :error" do
303
+ client = new_client(:log_level => :error)
304
+ level = client.globals[:logger].level
305
+
306
+ expect(level).to eq(3)
307
+ end
308
+
309
+ it "allows changing the Logger's log level to :fatal" do
310
+ client = new_client(:log_level => :fatal)
311
+ level = client.globals[:logger].level
312
+
313
+ expect(level).to eq(4)
314
+ end
315
+
316
+ it "raises when the given level is not valid" do
317
+ expect { new_client(:log_level => :invalid) }.
318
+ to raise_error(ArgumentError, /Invalid log level: :invalid/)
319
+ end
320
+ end
321
+
322
+ context "global :ssl_version" do
323
+ it "sets the SSL version to use" do
324
+ HTTPI::Auth::SSL.any_instance.expects(:ssl_version=).with(:SSLv3).twice
325
+
326
+ client = new_client(:endpoint => @server.url, :ssl_version => :SSLv3)
327
+ client.call(:authenticate)
328
+ end
329
+ end
330
+
331
+ context "global :ssl_verify_mode" do
332
+ it "sets the verify mode to use" do
333
+ HTTPI::Auth::SSL.any_instance.expects(:verify_mode=).with(:none).twice
334
+
335
+ client = new_client(:endpoint => @server.url, :ssl_verify_mode => :none)
336
+ client.call(:authenticate)
337
+ end
338
+ end
339
+
340
+ context "global :ssl_cert_key_file" do
341
+ it "sets the cert key file to use" do
342
+ cert_key = File.expand_path("../../fixtures/ssl/client_key.pem", __FILE__)
343
+ HTTPI::Auth::SSL.any_instance.expects(:cert_key_file=).with(cert_key).twice
344
+
345
+ client = new_client(:endpoint => @server.url, :ssl_cert_key_file => cert_key)
346
+ client.call(:authenticate)
347
+ end
348
+ end
349
+
350
+ context "global :ssl_cert_key_password" do
351
+ it "sets the encrypted cert key file password to use" do
352
+ cert_key = File.expand_path("../../fixtures/ssl/client_encrypted_key.pem", __FILE__)
353
+ cert_key_pass = "secure-password!42"
354
+ HTTPI::Auth::SSL.any_instance.expects(:cert_key_file=).with(cert_key).twice
355
+ HTTPI::Auth::SSL.any_instance.expects(:cert_key_password=).with(cert_key_pass).twice
356
+
357
+ client = new_client(:endpoint => @server.url, :ssl_cert_key_file => cert_key, :ssl_cert_key_password => cert_key_pass)
358
+ client.call(:authenticate)
359
+ end
360
+
361
+ end
362
+
363
+ context "global :ssl_cert_file" do
364
+ it "sets the cert file to use" do
365
+ cert = File.expand_path("../../fixtures/ssl/client_cert.pem", __FILE__)
366
+ HTTPI::Auth::SSL.any_instance.expects(:cert_file=).with(cert).twice
367
+
368
+ client = new_client(:endpoint => @server.url, :ssl_cert_file => cert)
369
+ client.call(:authenticate)
370
+ end
371
+ end
372
+
373
+ context "global :ssl_ca_cert_file" do
374
+ it "sets the ca cert file to use" do
375
+ ca_cert = File.expand_path("../../fixtures/ssl/client_cert.pem", __FILE__)
376
+ HTTPI::Auth::SSL.any_instance.expects(:ca_cert_file=).with(ca_cert).twice
377
+
378
+ client = new_client(:endpoint => @server.url, :ssl_ca_cert_file => ca_cert)
379
+ client.call(:authenticate)
380
+ end
381
+ end
382
+
383
+ context "global :basic_auth" do
384
+ it "sets the basic auth credentials" do
385
+ client = new_client(:endpoint => @server.url(:basic_auth), :basic_auth => ["admin", "secret"])
386
+ response = client.call(:authenticate)
387
+
388
+ expect(response.http.body).to eq("basic-auth")
389
+ end
390
+ end
391
+
392
+ context "global :digest_auth" do
393
+ it "sets the digest auth credentials" do
394
+ client = new_client(:endpoint => @server.url(:digest_auth), :digest_auth => ["admin", "secret"])
395
+ response = client.call(:authenticate)
396
+
397
+ expect(response.http.body).to eq("digest-auth")
398
+ end
399
+ end
400
+
401
+ context "global :ntlm" do
402
+ it "sets the ntlm credentials to use" do
403
+ credentials = ["admin", "secret"]
404
+ client = new_client(:endpoint => @server.url, :ntlm => credentials)
405
+
406
+ # TODO: find a way to integration test this. including an entire ntlm
407
+ # server implementation seems a bit over the top though.
408
+ HTTPI::Auth::Config.any_instance.expects(:ntlm).with(*credentials)
409
+
410
+ response = client.call(:authenticate)
411
+ end
412
+ end
413
+
414
+ context "global :filters" do
415
+ it "filters a list of XML tags from logged SOAP messages" do
416
+ silence_stdout do
417
+ client = new_client(:endpoint => @server.url(:repeat), :log => true)
418
+
419
+ client.globals[:filters] << :password
420
+
421
+ # filter out logs we're not interested in
422
+ client.globals[:logger].expects(:info).at_least_once
423
+
424
+ # check whether the password is filtered
425
+ client.globals[:logger].expects(:debug).with { |message|
426
+ message.include? "<password>***FILTERED***</password>"
427
+ }.twice
428
+
429
+ message = { :username => "luke", :password => "secret" }
430
+ client.call(:authenticate, :message => message)
431
+ end
432
+ end
433
+ end
434
+
435
+ context "global :pretty_print_xml" do
436
+ it "is a nice but expensive way to debug XML messages" do
437
+ silence_stdout do
438
+ client = new_client(:endpoint => @server.url(:repeat), :pretty_print_xml => true, :log => true)
439
+
440
+ # filter out logs we're not interested in
441
+ client.globals[:logger].expects(:info).at_least_once
442
+
443
+ # check whether the message is pretty printed
444
+ client.globals[:logger].expects(:debug).with { |message|
445
+ envelope = message =~ /\n<env:Envelope/
446
+ body = message =~ /\n <env:Body>/
447
+ message_tag = message =~ /\n <tns:authenticate\/>/
448
+
449
+ envelope && body && message_tag
450
+ }.twice
451
+
452
+ client.call(:authenticate)
453
+ end
454
+ end
455
+ end
456
+
457
+ context "global :wsse_auth" do
458
+ it "adds WSSE basic auth information to the request" do
459
+ client = new_client(:endpoint => @server.url(:repeat), :wsse_auth => ["luke", "secret"])
460
+ response = client.call(:authenticate)
461
+
462
+ request = response.http.body
463
+
464
+ # the header and wsse security node
465
+ wsse_namespace = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
466
+ expect(request).to include("<env:Header><wsse:Security xmlns:wsse=\"#{wsse_namespace}\">")
467
+
468
+ # split up to prevent problems with unordered Hash attributes in 1.8 [dh, 2012-12-13]
469
+ expect(request).to include("<wsse:UsernameToken")
470
+ expect(request).to include("wsu:Id=\"UsernameToken-1\"")
471
+ expect(request).to include("xmlns:wsu=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd\"")
472
+
473
+ # the username and password node with type attribute
474
+ expect(request).to include("<wsse:Username>luke</wsse:Username>")
475
+ password_text = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText"
476
+ expect(request).to include("<wsse:Password Type=\"#{password_text}\">secret</wsse:Password>")
477
+ end
478
+
479
+ it "adds WSSE digest auth information to the request" do
480
+ client = new_client(:endpoint => @server.url(:repeat), :wsse_auth => ["lea", "top-secret", :digest])
481
+ response = client.call(:authenticate)
482
+
483
+ request = response.http.body
484
+
485
+ # the header and wsse security node
486
+ wsse_namespace = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
487
+ expect(request).to include("<env:Header><wsse:Security xmlns:wsse=\"#{wsse_namespace}\">")
488
+
489
+ # split up to prevent problems with unordered Hash attributes in 1.8 [dh, 2012-12-13]
490
+ expect(request).to include("<wsse:UsernameToken")
491
+ expect(request).to include("wsu:Id=\"UsernameToken-1\"")
492
+ expect(request).to include("xmlns:wsu=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd\"")
493
+
494
+ # the username node
495
+ expect(request).to include("<wsse:Username>lea</wsse:Username>")
496
+
497
+ # the nonce node
498
+ expect(request).to match(/<wsse:Nonce>.+<\/wsse:Nonce>/)
499
+
500
+ # the created node with a timestamp
501
+ expect(request).to match(/<wsu:Created>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.*<\/wsu:Created>/)
502
+
503
+ # the password node contains the encrypted value
504
+ password_digest = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest"
505
+ expect(request).to match(/<wsse:Password Type=\"#{password_digest}\">.+<\/wsse:Password>/)
506
+ expect(request).to_not include("top-secret")
507
+ end
508
+ end
509
+
510
+ context "global :wsse_timestamp" do
511
+ it "adds WSSE timestamp auth information to the request" do
512
+ client = new_client(:endpoint => @server.url(:repeat), :wsse_timestamp => true)
513
+ response = client.call(:authenticate)
514
+
515
+ request = response.http.body
516
+
517
+ # the header and wsse security node
518
+ wsse_namespace = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
519
+ expect(request).to include("<env:Header><wsse:Security xmlns:wsse=\"#{wsse_namespace}\">")
520
+
521
+ # split up to prevent problems with unordered Hash attributes in 1.8 [dh, 2012-12-13]
522
+ expect(request).to include("<wsu:Timestamp")
523
+ expect(request).to include("wsu:Id=\"Timestamp-1\"")
524
+ expect(request).to include("xmlns:wsu=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd\"")
525
+
526
+ # the created node with a timestamp
527
+ expect(request).to match(/<wsu:Created>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.*<\/wsu:Created>/)
528
+
529
+ # the expires node with a timestamp
530
+ expect(request).to match(/<wsu:Expires>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.*<\/wsu:Expires>/)
531
+ end
532
+ end
533
+
534
+ context "global :strip_namespaces" do
535
+ it "can be changed to not strip any namespaces" do
536
+ client = new_client(
537
+ :endpoint => @server.url(:repeat),
538
+ :convert_response_tags_to => lambda { |tag| tag.snakecase },
539
+ :strip_namespaces => false
540
+ )
541
+
542
+ response = client.call(:authenticate, :xml => Fixture.response(:authentication))
543
+
544
+ expect(response.hash["soap:envelope"]["soap:body"]).to include("ns2:authenticate_response")
545
+ end
546
+ end
547
+
548
+ context "global :convert_request_keys_to" do
549
+ it "changes how Hash message key Symbols are translated to XML tags for the request" do
550
+ client = new_client_without_wsdl do |globals|
551
+ globals.endpoint @server.url(:repeat)
552
+ globals.namespace "http://v1.example.com"
553
+ globals.convert_request_keys_to :camelcase # or one of [:lower_camelcase, :upcase, :none]
554
+ end
555
+
556
+ response = client.call(:find_user) do |locals|
557
+ locals.message(:user_name => "luke", "pass_word" => "secret")
558
+ end
559
+
560
+ request = response.http.body
561
+
562
+ # split into multiple assertions thanks to 1.8
563
+ expect(request).to include("<wsdl:FindUser>")
564
+ expect(request).to include("<UserName>luke</UserName>")
565
+ expect(request).to include("<pass_word>secret</pass_word>")
566
+ end
567
+ end
568
+
569
+ context "global :convert_response_tags_to" do
570
+ it "changes how XML tags from the SOAP response are translated into Hash keys" do
571
+ client = new_client(:endpoint => @server.url(:repeat), :convert_response_tags_to => lambda { |tag| tag.snakecase.upcase })
572
+ response = client.call(:authenticate, :xml => Fixture.response(:authentication))
573
+
574
+ expect(response.hash["ENVELOPE"]["BODY"]).to include("AUTHENTICATE_RESPONSE")
575
+ end
576
+
577
+ it "accepts a block in the block-based interface" do
578
+ client = Savon.client do |globals|
579
+ globals.log false
580
+ globals.wsdl Fixture.wsdl(:authentication)
581
+ globals.endpoint @server.url(:repeat)
582
+ globals.convert_response_tags_to { |tag| tag.snakecase.upcase }
583
+ end
584
+
585
+ response = client.call(:authenticate) do |locals|
586
+ locals.xml Fixture.response(:authentication)
587
+ end
588
+
589
+ expect(response.hash["ENVELOPE"]["BODY"]).to include("AUTHENTICATE_RESPONSE")
590
+ end
591
+ end
592
+
593
+ context "global and request :soap_header" do
594
+ it "merges the headers if both were provided as Hashes" do
595
+ global_soap_header = {
596
+ :global_header => { :auth_token => "secret" },
597
+ :merged => { :global => true }
598
+ }
599
+
600
+ request_soap_header = {
601
+ :request_header => { :auth_token => "secret" },
602
+ :merged => { :request => true }
603
+ }
604
+
605
+ client = new_client(:endpoint => @server.url(:repeat), :soap_header => global_soap_header)
606
+
607
+ response = client.call(:authenticate, :soap_header => request_soap_header)
608
+ request_body = response.http.body
609
+
610
+ expect(request_body).to include("<globalHeader><authToken>secret</authToken></globalHeader>")
611
+ expect(request_body).to include("<requestHeader><authToken>secret</authToken></requestHeader>")
612
+ expect(request_body).to include("<merged><request>true</request></merged>")
613
+ end
614
+
615
+ it "prefers the request over the global option if at least one of them is not a Hash" do
616
+ global_soap_header = "<global>header</global>"
617
+ request_soap_header = "<request>header</request>"
618
+
619
+ client = new_client(:endpoint => @server.url(:repeat), :soap_header => global_soap_header)
620
+
621
+ response = client.call(:authenticate, :soap_header => request_soap_header)
622
+ request_body = response.http.body
623
+
624
+ expect(request_body).to include("<env:Header><request>header</request></env:Header>")
625
+ end
626
+ end
627
+
628
+ context "request :soap_header" do
629
+ it "accepts a Hash of SOAP header information" do
630
+ client = new_client(:endpoint => @server.url(:repeat))
631
+
632
+ response = client.call(:authenticate, :soap_header => { :auth_token => "secret" })
633
+ expect(response.http.body).to include("<env:Header><authToken>secret</authToken></env:Header>")
634
+ end
635
+
636
+ it "accepts anything other than a String and calls #to_s on it" do
637
+ to_s_header = Class.new {
638
+ def to_s
639
+ "to_s_header"
640
+ end
641
+ }.new
642
+
643
+ client = new_client(:endpoint => @server.url(:repeat))
644
+
645
+ response = client.call(:authenticate, :soap_header => to_s_header)
646
+ expect(response.http.body).to include("<env:Header>to_s_header</env:Header>")
647
+ end
648
+ end
649
+
650
+ context "request: message_tag" do
651
+ it "when set, changes the SOAP message tag" do
652
+ response = new_client(:endpoint => @server.url(:repeat)).call(:authenticate, :message_tag => :doAuthenticate)
653
+ expect(response.http.body).to include("<tns:doAuthenticate></tns:doAuthenticate>")
654
+ end
655
+
656
+ it "without it, Savon tries to get the message tag from the WSDL document" do
657
+ response = new_client(:endpoint => @server.url(:repeat)).call(:authenticate)
658
+ expect(response.http.body).to include("<tns:authenticate></tns:authenticate>")
659
+ end
660
+
661
+ it "without the option and a WSDL, Savon defaults to Gyoku to create the name" do
662
+ client = Savon.client(:endpoint => @server.url(:repeat), :namespace => "http://v1.example.com", :log => false)
663
+
664
+ response = client.call(:init_authentication)
665
+ expect(response.http.body).to include("<wsdl:initAuthentication></wsdl:initAuthentication>")
666
+ end
667
+ end
668
+
669
+ context "request: attributes" do
670
+ it "when set, adds the attributes to the message tag" do
671
+ client = new_client(:endpoint => @server.url(:repeat))
672
+ response = client.call(:authenticate, :attributes => { "Token" => "secret"})
673
+
674
+ expect(response.http.body).to include('<tns:authenticate Token="secret">')
675
+ end
676
+ end
677
+
678
+ context "request: soap_action" do
679
+ it "without it, Savon tries to get the SOAPAction from the WSDL document and falls back to Gyoku" do
680
+ client = new_client(:endpoint => @server.url(:inspect_request))
681
+
682
+ response = client.call(:authenticate)
683
+ soap_action = inspect_request(response).soap_action
684
+ expect(soap_action).to eq('"authenticate"')
685
+ end
686
+
687
+ it "when set, changes the SOAPAction HTTP header" do
688
+ client = new_client(:endpoint => @server.url(:inspect_request))
689
+
690
+ response = client.call(:authenticate, :soap_action => "doAuthenticate")
691
+ soap_action = inspect_request(response).soap_action
692
+ expect(soap_action).to eq('"doAuthenticate"')
693
+ end
694
+ end
695
+
696
+ context "request :message" do
697
+ it "accepts a Hash which is passed to Gyoku to be converted to XML" do
698
+ response = new_client(:endpoint => @server.url(:repeat)).call(:authenticate, :message => { :user => "luke", :password => "secret" })
699
+
700
+ request = response.http.body
701
+ expect(request).to include("<user>luke</user>")
702
+ expect(request).to include("<password>secret</password>")
703
+ end
704
+
705
+ it "also accepts a String of raw XML" do
706
+ response = new_client(:endpoint => @server.url(:repeat)).call(:authenticate, :message => "<user>lea</user><password>top-secret</password>")
707
+ expect(response.http.body).to include("<tns:authenticate><user>lea</user><password>top-secret</password></tns:authenticate>")
708
+ end
709
+ end
710
+
711
+ context "request :xml" do
712
+ it "accepts a String of raw XML" do
713
+ response = new_client(:endpoint => @server.url(:repeat)).call(:authenticate, :xml => "<soap>request</soap>")
714
+ expect(response.http.body).to eq("<soap>request</soap>")
715
+ end
716
+ end
717
+
718
+ context "request :cookies" do
719
+ it "accepts an Array of HTTPI::Cookie objects for the next request" do
720
+ cookies = [
721
+ HTTPI::Cookie.new("some-cookie=choc-chip"),
722
+ HTTPI::Cookie.new("another-cookie=ny-cheesecake")
723
+ ]
724
+
725
+ client = new_client(:endpoint => @server.url(:inspect_request))
726
+ response = client.call(:authenticate, :cookies => cookies)
727
+
728
+ cookie = inspect_request(response).cookie
729
+ expect(cookie.split(";")).to include(
730
+ "some-cookie=choc-chip",
731
+ "another-cookie=ny-cheesecake"
732
+ )
733
+ end
734
+ end
735
+
736
+ context "request :advanced_typecasting" do
737
+ it "can be changed to false to disable Nori's advanced typecasting" do
738
+ client = new_client(:endpoint => @server.url(:repeat))
739
+ response = client.call(:authenticate, :xml => Fixture.response(:authentication), :advanced_typecasting => false)
740
+
741
+ expect(response.body[:authenticate_response][:return][:success]).to eq("true")
742
+ end
743
+ end
744
+
745
+ context "request :response_parser" do
746
+ it "instructs Nori to change the response parser" do
747
+ nori = Nori.new(:strip_namespaces => true, :convert_tags_to => lambda { |tag| tag.snakecase.to_sym })
748
+ Nori.expects(:new).with { |options| options[:parser] == :nokogiri }.returns(nori)
749
+
750
+ client = new_client(:endpoint => @server.url(:repeat))
751
+ response = client.call(:authenticate, :xml => Fixture.response(:authentication), :response_parser => :nokogiri)
752
+
753
+ expect(response.body).to_not be_empty
754
+ end
755
+ end
756
+
757
+ def new_client(globals = {}, &block)
758
+ globals = { :wsdl => Fixture.wsdl(:authentication), :log => false }.merge(globals)
759
+ Savon.client(globals, &block)
760
+ end
761
+
762
+ def new_client_without_wsdl(globals = {}, &block)
763
+ globals = { :log => false }.merge(globals)
764
+ Savon.client(globals, &block)
765
+ end
766
+
767
+ def inspect_request(response)
768
+ hash = JSON.parse(response.http.body)
769
+ OpenStruct.new(hash)
770
+ end
771
+
772
+ end