savon_with_adapter 2.4.1

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