savon 1.2.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. data/CHANGELOG.md +119 -104
  2. data/README.md +12 -11
  3. data/Rakefile +0 -6
  4. data/lib/savon.rb +16 -14
  5. data/lib/savon/block_interface.rb +26 -0
  6. data/lib/savon/builder.rb +142 -0
  7. data/lib/savon/client.rb +36 -135
  8. data/lib/savon/header.rb +42 -0
  9. data/lib/savon/http_error.rb +27 -0
  10. data/lib/savon/log_message.rb +23 -25
  11. data/lib/savon/message.rb +35 -0
  12. data/lib/savon/mock.rb +5 -0
  13. data/lib/savon/mock/expectation.rb +70 -0
  14. data/lib/savon/mock/spec_helper.rb +62 -0
  15. data/lib/savon/model.rb +39 -61
  16. data/lib/savon/operation.rb +62 -0
  17. data/lib/savon/options.rb +265 -0
  18. data/lib/savon/qualified_message.rb +49 -0
  19. data/lib/savon/request.rb +92 -0
  20. data/lib/savon/response.rb +97 -0
  21. data/lib/savon/soap_fault.rb +40 -0
  22. data/lib/savon/version.rb +1 -1
  23. data/savon.gemspec +10 -8
  24. data/spec/integration/options_spec.rb +536 -0
  25. data/spec/integration/request_spec.rb +31 -16
  26. data/spec/integration/support/application.rb +80 -0
  27. data/spec/integration/support/server.rb +84 -0
  28. data/spec/savon/builder_spec.rb +81 -0
  29. data/spec/savon/client_spec.rb +90 -488
  30. data/spec/savon/http_error_spec.rb +49 -0
  31. data/spec/savon/log_message_spec.rb +33 -0
  32. data/spec/savon/mock_spec.rb +127 -0
  33. data/spec/savon/model_spec.rb +110 -99
  34. data/spec/savon/observers_spec.rb +92 -0
  35. data/spec/savon/operation_spec.rb +49 -0
  36. data/spec/savon/request_spec.rb +145 -0
  37. data/spec/savon/{soap/response_spec.rb → response_spec.rb} +22 -59
  38. data/spec/savon/soap_fault_spec.rb +94 -0
  39. data/spec/spec_helper.rb +5 -3
  40. data/spec/support/fixture.rb +5 -1
  41. metadata +202 -197
  42. data/lib/savon/config.rb +0 -46
  43. data/lib/savon/error.rb +0 -6
  44. data/lib/savon/hooks/group.rb +0 -68
  45. data/lib/savon/hooks/hook.rb +0 -61
  46. data/lib/savon/http/error.rb +0 -42
  47. data/lib/savon/logger.rb +0 -39
  48. data/lib/savon/null_logger.rb +0 -10
  49. data/lib/savon/soap.rb +0 -21
  50. data/lib/savon/soap/fault.rb +0 -59
  51. data/lib/savon/soap/invalid_response_error.rb +0 -13
  52. data/lib/savon/soap/request.rb +0 -86
  53. data/lib/savon/soap/request_builder.rb +0 -205
  54. data/lib/savon/soap/response.rb +0 -117
  55. data/lib/savon/soap/xml.rb +0 -257
  56. data/spec/savon/config_spec.rb +0 -38
  57. data/spec/savon/hooks/group_spec.rb +0 -71
  58. data/spec/savon/hooks/hook_spec.rb +0 -16
  59. data/spec/savon/http/error_spec.rb +0 -52
  60. data/spec/savon/logger_spec.rb +0 -51
  61. data/spec/savon/savon_spec.rb +0 -33
  62. data/spec/savon/soap/fault_spec.rb +0 -89
  63. data/spec/savon/soap/request_builder_spec.rb +0 -207
  64. data/spec/savon/soap/request_spec.rb +0 -112
  65. data/spec/savon/soap/xml_spec.rb +0 -357
  66. data/spec/savon/soap_spec.rb +0 -16
@@ -0,0 +1,97 @@
1
+ require "nori"
2
+ require "savon/soap_fault"
3
+ require "savon/http_error"
4
+
5
+ module Savon
6
+ class Response
7
+
8
+ def initialize(http, globals, locals)
9
+ @http = http
10
+ @globals = globals
11
+ @locals = locals
12
+
13
+ raise_soap_and_http_errors! if @globals[:raise_errors]
14
+ end
15
+
16
+ attr_reader :http, :globals, :locals
17
+
18
+ def success?
19
+ !soap_fault? && !http_error?
20
+ end
21
+ alias successful? success?
22
+
23
+ def soap_fault?
24
+ SOAPFault.present? @http
25
+ end
26
+
27
+ def http_error?
28
+ HTTPError.present? @http
29
+ end
30
+
31
+ def header
32
+ raise_invalid_response_error! unless hash.key? :envelope
33
+ hash[:envelope][:header]
34
+ end
35
+
36
+ def body
37
+ raise_invalid_response_error! unless hash.key? :envelope
38
+ hash[:envelope][:body]
39
+ end
40
+ alias to_hash body
41
+
42
+ def to_array(*path)
43
+ result = path.inject body do |memo, key|
44
+ return [] if memo[key].nil?
45
+ memo[key]
46
+ end
47
+
48
+ result.kind_of?(Array) ? result.compact : [result].compact
49
+ end
50
+
51
+ def hash
52
+ @hash ||= nori.parse(to_xml)
53
+ end
54
+
55
+ def to_xml
56
+ @http.body
57
+ end
58
+
59
+ def doc
60
+ @doc ||= Nokogiri.XML(to_xml)
61
+ end
62
+
63
+ def xpath(path, namespaces = nil)
64
+ doc.xpath(path, namespaces || xml_namespaces)
65
+ end
66
+
67
+ private
68
+
69
+ def raise_soap_and_http_errors!
70
+ raise SOAPFault.new(@http, nori) if soap_fault?
71
+ raise HTTPError.new(@http) if http_error?
72
+ end
73
+
74
+ def raise_invalid_response_error!
75
+ raise InvalidResponseError, "Unable to parse response body:\n" + to_xml.inspect
76
+ end
77
+
78
+ def xml_namespaces
79
+ @xml_namespaces ||= doc.collect_namespaces
80
+ end
81
+
82
+ def nori
83
+ return @nori if @nori
84
+
85
+ nori_options = {
86
+ :strip_namespaces => @globals[:strip_namespaces],
87
+ :convert_tags_to => @globals[:convert_response_tags_to],
88
+ :advanced_typecasting => @locals[:advanced_typecasting],
89
+ :parser => @locals[:response_parser]
90
+ }
91
+
92
+ non_nil_nori_options = nori_options.reject { |_, value| value.nil? }
93
+ @nori = Nori.new(non_nil_nori_options)
94
+ end
95
+
96
+ end
97
+ end
@@ -0,0 +1,40 @@
1
+ require "savon"
2
+
3
+ module Savon
4
+ class SOAPFault < Error
5
+
6
+ def self.present?(http)
7
+ fault_node = http.body.include?("Fault>")
8
+ soap1_fault = http.body.include?("faultcode>") && http.body.include?("faultstring>")
9
+ soap2_fault = http.body.include?("Code>") && http.body.include?("Reason>")
10
+
11
+ fault_node && (soap1_fault || soap2_fault)
12
+ end
13
+
14
+ def initialize(http, nori)
15
+ @http = http
16
+ @nori = nori
17
+ end
18
+
19
+ attr_reader :http, :nori
20
+
21
+ def to_s
22
+ message_by_version to_hash[:fault]
23
+ end
24
+
25
+ def to_hash
26
+ nori.parse(@http.body)[:envelope][:body]
27
+ end
28
+
29
+ private
30
+
31
+ def message_by_version(fault)
32
+ if fault[:faultcode]
33
+ "(#{fault[:faultcode]}) #{fault[:faultstring]}"
34
+ elsif fault[:code]
35
+ "(#{fault[:code][:value]}) #{fault[:reason][:text]}"
36
+ end
37
+ end
38
+
39
+ end
40
+ end
@@ -1,5 +1,5 @@
1
1
  module Savon
2
2
 
3
- VERSION = "1.2.0"
3
+ VERSION = "2.0.0"
4
4
 
5
5
  end
@@ -15,19 +15,21 @@ Gem::Specification.new do |s|
15
15
 
16
16
  s.rubyforge_project = s.name
17
17
 
18
- s.add_dependency "nori", "~> 1.1.0"
19
- s.add_dependency "httpi", "~> 1.1.0"
20
- s.add_dependency "wasabi", "~> 2.5.0"
18
+ s.add_dependency "nori", "~> 2.0.0"
19
+ s.add_dependency "httpi", "~> 2.0.0"
20
+ s.add_dependency "wasabi", "~> 3.0.0"
21
21
  s.add_dependency "akami", "~> 1.2.0"
22
- s.add_dependency "gyoku", "~> 0.4.5"
22
+ s.add_dependency "gyoku", "~> 1.0.0"
23
23
 
24
24
  s.add_dependency "builder", ">= 2.1.2"
25
25
  s.add_dependency "nokogiri", ">= 1.4.0"
26
26
 
27
- s.add_development_dependency "rake", "~> 0.9"
28
- s.add_development_dependency "rspec", "~> 2.10"
29
- s.add_development_dependency "mocha", "~> 0.11"
30
- s.add_development_dependency "timecop", "~> 0.3"
27
+ s.add_development_dependency "rack"
28
+ s.add_development_dependency "puma", ">= 2.0.0.b3"
29
+
30
+ s.add_development_dependency "rake", "~> 0.9"
31
+ s.add_development_dependency "rspec", "~> 2.10"
32
+ s.add_development_dependency "mocha", "~> 0.11"
31
33
 
32
34
  s.files = `git ls-files`.split("\n")
33
35
  s.require_path = "lib"
@@ -0,0 +1,536 @@
1
+ require "spec_helper"
2
+ require "integration/support/server"
3
+
4
+ describe "Options" do
5
+
6
+ before :all do
7
+ @server = IntegrationServer.run
8
+ end
9
+
10
+ after :all do
11
+ @server.stop
12
+ end
13
+
14
+ context "global: endpoint and namespace" do
15
+ it "sets the SOAP endpoint to use to allow requests without a WSDL document" do
16
+ client = new_client_without_wsdl(:endpoint => @server.url(:repeat), :namespace => "http://v1.example.com")
17
+ response = client.call(:authenticate)
18
+
19
+ # the default namespace identifier is :wsdl and contains the namespace option
20
+ expect(response.http.body).to include('xmlns:wsdl="http://v1.example.com"')
21
+
22
+ # the default namespace applies to the message tag
23
+ expect(response.http.body).to include('<wsdl:authenticate>')
24
+ end
25
+ end
26
+
27
+ context "global :namespace_identifier" do
28
+ it "changes the default namespace identifier" do
29
+ client = new_client(:endpoint => @server.url(:repeat), :namespace_identifier => :lol)
30
+ response = client.call(:authenticate)
31
+
32
+ expect(response.http.body).to include('xmlns:lol="http://v1_0.ws.auth.order.example.com/"')
33
+ expect(response.http.body).to include("<lol:authenticate></lol:authenticate>")
34
+ end
35
+ end
36
+
37
+ context "global :proxy" do
38
+ it "sets the proxy server to use" do
39
+ proxy_url = "http://example.com"
40
+ client = new_client(:endpoint => @server.url, :proxy => proxy_url)
41
+
42
+ # TODO: find a way to integration test this [dh, 2012-12-08]
43
+ HTTPI::Request.any_instance.expects(:proxy=).with(proxy_url)
44
+
45
+ response = client.call(:authenticate)
46
+ end
47
+ end
48
+
49
+ context "global :headers" do
50
+ it "sets the HTTP headers for the next request" do
51
+ client = new_client(:endpoint => @server.url(:repeat_header), :headers => { "Repeat-Header" => "savon" })
52
+
53
+ response = client.call(:authenticate)
54
+ expect(response.http.body).to eq("savon")
55
+ end
56
+ end
57
+
58
+ context "global :open_timeout" do
59
+ it "makes the client timeout after n seconds" do
60
+ non_routable_ip = "http://10.255.255.1"
61
+ client = new_client(:endpoint => non_routable_ip, :open_timeout => 1)
62
+
63
+ # TODO: make HTTPI tag timeout errors, then depend on HTTPI::TimeoutError instead of a specific client error [dh, 2012-12-08]
64
+ expect { client.call(:authenticate) }.to raise_error(HTTPClient::ConnectTimeoutError)
65
+ end
66
+ end
67
+
68
+ context "global :read_timeout" do
69
+ it "makes the client timeout after n seconds" do
70
+ client = new_client(:endpoint => @server.url(:timeout), :open_timeout => 1, :read_timeout => 1)
71
+
72
+ expect { client.call(:authenticate) }.to raise_error(HTTPClient::ReceiveTimeoutError)
73
+ end
74
+ end
75
+
76
+ context "global :encoding" do
77
+ it "changes the XML instruction" do
78
+ client = new_client(:endpoint => @server.url(:repeat), :encoding => "UTF-16")
79
+ response = client.call(:authenticate)
80
+
81
+ expect(response.http.body).to match(/<\?xml version="1\.0" encoding="UTF-16"\?>/)
82
+ end
83
+
84
+ it "changes the Content-Type header" do
85
+ client = new_client(:endpoint => @server.url(:inspect_header), :encoding => "UTF-16",
86
+ :headers => { "Inspect" => "CONTENT_TYPE" })
87
+
88
+ response = client.call(:authenticate)
89
+ expect(response.http.body).to eq("text/xml;charset=UTF-16")
90
+ end
91
+ end
92
+
93
+ context "global :soap_header" do
94
+ it "accepts a Hash of SOAP header information" do
95
+ client = new_client(:endpoint => @server.url(:repeat), :soap_header => { :auth_token => "secret" })
96
+
97
+ response = client.call(:authenticate)
98
+ expect(response.http.body).to include("<env:Header><authToken>secret</authToken></env:Header>")
99
+ end
100
+ end
101
+
102
+ context "global :element_form_default" do
103
+ it "specifies whether elements should be :qualified or :unqualified" do
104
+ # qualified
105
+ client = new_client(:endpoint => @server.url(:repeat), :element_form_default => :qualified)
106
+
107
+ response = client.call(:authenticate, :message => { :user => "luke", :password => "secret" })
108
+ expect(response.http.body).to include("<tns:user>luke</tns:user>")
109
+ expect(response.http.body).to include("<tns:password>secret</tns:password>")
110
+
111
+ # unqualified
112
+ client = new_client(:endpoint => @server.url(:repeat), :element_form_default => :unqualified)
113
+
114
+ response = client.call(:authenticate, :message => { :user => "lea", :password => "top-secret" })
115
+ expect(response.http.body).to include("<user>lea</user>")
116
+ expect(response.http.body).to include("<password>top-secret</password>")
117
+ end
118
+
119
+ it "allows overwriting the SOAPAction HTTP header" do
120
+ client = new_client(:endpoint => @server.url(:inspect_header),
121
+ :headers => { "Inspect" => "HTTP_SOAPACTION" })
122
+
123
+ response = client.call(:authenticate)
124
+ expect(response.http.body).to eq('"authenticate"')
125
+ end
126
+ end
127
+
128
+ context "global :env_namespace" do
129
+ it "when set, replaces the default namespace identifier for the SOAP envelope" do
130
+ client = new_client(:endpoint => @server.url(:repeat), :env_namespace => "soapenv")
131
+ response = client.call(:authenticate)
132
+
133
+ expect(response.http.body).to include("<soapenv:Envelope")
134
+ end
135
+
136
+ it "when not set, Savon defaults to use :env as the namespace identifier for the SOAP envelope" do
137
+ client = new_client(:endpoint => @server.url(:repeat))
138
+ response = client.call(:authenticate)
139
+
140
+ expect(response.http.body).to include("<env:Envelope")
141
+ end
142
+ end
143
+
144
+ context "global :soap_version" do
145
+ it "it uses the correct SOAP 1.1 namespace" do
146
+ client = new_client(:endpoint => @server.url(:repeat), :soap_version => 1)
147
+ response = client.call(:authenticate)
148
+
149
+ expect(response.http.body).to include('xmlns:env="http://schemas.xmlsoap.org/soap/envelope/"')
150
+ end
151
+
152
+ it "it uses the correct SOAP 1.2 namespace" do
153
+ client = new_client(:endpoint => @server.url(:repeat), :soap_version => 2)
154
+ response = client.call(:authenticate)
155
+
156
+ expect(response.http.body).to include('xmlns:env="http://www.w3.org/2003/05/soap-envelope"')
157
+ end
158
+ end
159
+
160
+ context "global: raise_errors" do
161
+ it "when true, instructs Savon to raise SOAP fault errors" do
162
+ client = new_client(:endpoint => @server.url(:repeat), :raise_errors => true)
163
+
164
+ expect { client.call(:authenticate, :xml => Fixture.response(:soap_fault)) }.
165
+ to raise_error(Savon::SOAPFault)
166
+
167
+ begin
168
+ client.call(:authenticate, :xml => Fixture.response(:soap_fault))
169
+ rescue Savon::SOAPFault => soap_fault
170
+ # check whether the configured nori instance is used by the soap fault
171
+ expect(soap_fault.to_hash[:fault][:faultcode]).to eq("soap:Server")
172
+ end
173
+ end
174
+
175
+ it "when true, instructs Savon to raise HTTP errors" do
176
+ client = new_client(:endpoint => @server.url(404), :raise_errors => true)
177
+ expect { client.call(:authenticate) }.to raise_error(Savon::HTTPError)
178
+ end
179
+
180
+ it "when false, instructs Savon to not raise SOAP fault errors" do
181
+ client = new_client(:endpoint => @server.url(:repeat), :raise_errors => false)
182
+ response = client.call(:authenticate, :xml => Fixture.response(:soap_fault))
183
+
184
+ expect(response).to_not be_successful
185
+ expect(response).to be_a_soap_fault
186
+ end
187
+
188
+ it "when false, instructs Savon to not raise HTTP errors" do
189
+ client = new_client(:endpoint => @server.url(404), :raise_errors => false)
190
+ response = client.call(:authenticate)
191
+
192
+ expect(response).to_not be_successful
193
+ expect(response).to be_a_http_error
194
+ end
195
+ end
196
+
197
+ context "global :log" do
198
+ it "silences HTTPI" do
199
+ HTTPI.expects(:log=).with(false)
200
+ new_client(:log => false)
201
+ end
202
+
203
+ it "turns HTTPI logging back on as well" do
204
+ HTTPI.expects(:log=).with(true)
205
+ new_client(:log => true)
206
+ end
207
+ end
208
+
209
+ context "global :logger" do
210
+ it "defaults to an instance of Ruby's standard Logger" do
211
+ logger = new_client.globals[:logger]
212
+ expect(logger).to be_a(Logger)
213
+ end
214
+ end
215
+
216
+ context "global :log_level" do
217
+ it "allows changing the Logger's log level to :debug" do
218
+ client = new_client(:log_level => :debug)
219
+ level = client.globals[:logger].level
220
+
221
+ expect(level).to eq(0)
222
+ end
223
+
224
+ it "allows changing the Logger's log level to :info" do
225
+ client = new_client(:log_level => :info)
226
+ level = client.globals[:logger].level
227
+
228
+ expect(level).to eq(1)
229
+ end
230
+
231
+ it "allows changing the Logger's log level to :warn" do
232
+ client = new_client(:log_level => :warn)
233
+ level = client.globals[:logger].level
234
+
235
+ expect(level).to eq(2)
236
+ end
237
+
238
+ it "allows changing the Logger's log level to :error" do
239
+ client = new_client(:log_level => :error)
240
+ level = client.globals[:logger].level
241
+
242
+ expect(level).to eq(3)
243
+ end
244
+
245
+ it "allows changing the Logger's log level to :fatal" do
246
+ client = new_client(:log_level => :fatal)
247
+ level = client.globals[:logger].level
248
+
249
+ expect(level).to eq(4)
250
+ end
251
+
252
+ it "raises when the given level is not valid" do
253
+ expect { new_client(:log_level => :invalid) }.
254
+ to raise_error(ArgumentError, /Invalid log level: :invalid/)
255
+ end
256
+ end
257
+
258
+ context "global :basic_auth" do
259
+ it "sets the basic auth credentials" do
260
+ client = new_client(:endpoint => @server.url(:basic_auth), :basic_auth => ["admin", "secret"])
261
+ response = client.call(:authenticate)
262
+
263
+ expect(response.http.body).to eq("basic-auth")
264
+ end
265
+ end
266
+
267
+ context "global :digest_auth" do
268
+ it "sets the digest auth credentials" do
269
+ client = new_client(:endpoint => @server.url(:digest_auth), :digest_auth => ["admin", "secret"])
270
+ response = client.call(:authenticate)
271
+
272
+ expect(response.http.body).to eq("digest-auth")
273
+ end
274
+ end
275
+
276
+ context "global :filters" do
277
+ it "filters a list of XML tags from logged SOAP messages" do
278
+ client = new_client(:endpoint => @server.url(:repeat), :log => true)
279
+
280
+ client.globals[:filters] << :password
281
+
282
+ # filter out logs we're not interested in
283
+ client.globals[:logger].expects(:info).at_least_once
284
+
285
+ # check whether the password is filtered
286
+ client.globals[:logger].expects(:debug).with { |message|
287
+ message.include? "<password>***FILTERED***</password>"
288
+ }.twice
289
+
290
+ message = { :username => "luke", :password => "secret" }
291
+ client.call(:authenticate, :message => message)
292
+ end
293
+ end
294
+
295
+ context "global :pretty_print_xml" do
296
+ it "is a nice but expensive way to debug XML messages" do
297
+ client = new_client(:endpoint => @server.url(:repeat), :pretty_print_xml => true, :log => true)
298
+
299
+ # filter out logs we're not interested in
300
+ client.globals[:logger].expects(:info).at_least_once
301
+
302
+ # check whether the message is pretty printed
303
+ client.globals[:logger].expects(:debug).with { |message|
304
+ envelope = message =~ /\n<env:Envelope/
305
+ body = message =~ /\n <env:Body>/
306
+ message_tag = message =~ /\n <tns:authenticate\/>/
307
+
308
+ envelope && body && message_tag
309
+ }.twice
310
+
311
+ client.call(:authenticate)
312
+ end
313
+ end
314
+
315
+ context "global :wsse_auth" do
316
+ it "adds WSSE basic auth information to the request" do
317
+ client = new_client(:endpoint => @server.url(:repeat), :wsse_auth => ["luke", "secret"])
318
+ response = client.call(:authenticate)
319
+
320
+ request = response.http.body
321
+
322
+ # the header and wsse security node
323
+ wsse_namespace = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
324
+ expect(request).to include("<env:Header><wsse:Security xmlns:wsse=\"#{wsse_namespace}\">")
325
+
326
+ # split up to prevent problems with unordered Hash attributes in 1.8 [dh, 2012-12-13]
327
+ expect(request).to include("<wsse:UsernameToken")
328
+ expect(request).to include("wsu:Id=\"UsernameToken-1\"")
329
+ expect(request).to include("xmlns:wsu=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd\"")
330
+
331
+ # the username and password node with type attribute
332
+ expect(request).to include("<wsse:Username>luke</wsse:Username>")
333
+ password_text = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText"
334
+ expect(request).to include("<wsse:Password Type=\"#{password_text}\">secret</wsse:Password>")
335
+ end
336
+
337
+ it "adds WSSE digest auth information to the request" do
338
+ client = new_client(:endpoint => @server.url(:repeat), :wsse_auth => ["lea", "top-secret", :digest])
339
+ response = client.call(:authenticate)
340
+
341
+ request = response.http.body
342
+
343
+ # the header and wsse security node
344
+ wsse_namespace = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
345
+ expect(request).to include("<env:Header><wsse:Security xmlns:wsse=\"#{wsse_namespace}\">")
346
+
347
+ # split up to prevent problems with unordered Hash attributes in 1.8 [dh, 2012-12-13]
348
+ expect(request).to include("<wsse:UsernameToken")
349
+ expect(request).to include("wsu:Id=\"UsernameToken-1\"")
350
+ expect(request).to include("xmlns:wsu=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd\"")
351
+
352
+ # the username node
353
+ expect(request).to include("<wsse:Username>lea</wsse:Username>")
354
+
355
+ # the nonce node
356
+ expect(request).to match(/<wsse:Nonce>.+<\/wsse:Nonce>/)
357
+
358
+ # the created node with a timestamp
359
+ expect(request).to match(/<wsu:Created>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.*<\/wsu:Created>/)
360
+
361
+ # the password node contains the encrypted value
362
+ password_digest = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest"
363
+ expect(request).to match(/<wsse:Password Type=\"#{password_digest}\">.+<\/wsse:Password>/)
364
+ expect(request).to_not include("top-secret")
365
+ end
366
+ end
367
+
368
+ context "global :wsse_timestamp" do
369
+ it "adds WSSE timestamp auth information to the request" do
370
+ client = new_client(:endpoint => @server.url(:repeat), :wsse_timestamp => true)
371
+ response = client.call(:authenticate)
372
+
373
+ request = response.http.body
374
+
375
+ # the header and wsse security node
376
+ wsse_namespace = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
377
+ expect(request).to include("<env:Header><wsse:Security xmlns:wsse=\"#{wsse_namespace}\">")
378
+
379
+ # split up to prevent problems with unordered Hash attributes in 1.8 [dh, 2012-12-13]
380
+ expect(request).to include("<wsu:Timestamp")
381
+ expect(request).to include("wsu:Id=\"Timestamp-1\"")
382
+ expect(request).to include("xmlns:wsu=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd\"")
383
+
384
+ # the created node with a timestamp
385
+ expect(request).to match(/<wsu:Created>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.*<\/wsu:Created>/)
386
+
387
+ # the expires node with a timestamp
388
+ expect(request).to match(/<wsu:Expires>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.*<\/wsu:Expires>/)
389
+ end
390
+ end
391
+
392
+ context "global :strip_namespaces" do
393
+ it "can be changed to not strip any namespaces" do
394
+ client = new_client(:endpoint => @server.url(:repeat), :convert_response_tags_to => lambda { |tag| tag.snakecase }, :strip_namespaces => false)
395
+ response = client.call(:authenticate, :xml => Fixture.response(:authentication))
396
+
397
+ # the header/body convenience methods fails when conventions are not met. [dh, 2012-12-12]
398
+ expect { response.body }.to raise_error(Savon::InvalidResponseError)
399
+
400
+ expect(response.hash["soap:envelope"]["soap:body"]).to include("ns2:authenticate_response")
401
+ end
402
+ end
403
+
404
+ context "global :convert_request_keys_to" do
405
+ it "changes how Hash message key Symbols are translated to XML tags for the request" do
406
+ client = new_client_without_wsdl do |globals|
407
+ globals.endpoint @server.url(:repeat)
408
+ globals.namespace "http://v1.example.com"
409
+ globals.convert_request_keys_to :camelcase # or one of [:lower_camelcase, :upcase, :none]
410
+ end
411
+
412
+ response = client.call(:find_user) do |locals|
413
+ locals.message(user_name: "luke", "pass_word" => "secret")
414
+ end
415
+
416
+ request = response.http.body
417
+
418
+ # split into multiple assertions thanks to 1.8
419
+ expect(request).to include("<wsdl:FindUser>")
420
+ expect(request).to include("<UserName>luke</UserName>")
421
+ expect(request).to include("<pass_word>secret</pass_word>")
422
+ end
423
+ end
424
+
425
+ context "global :convert_response_tags_to" do
426
+ it "changes how XML tags from the SOAP response are translated into Hash keys" do
427
+ client = new_client(:endpoint => @server.url(:repeat), :convert_response_tags_to => lambda { |tag| tag.snakecase.upcase })
428
+ response = client.call(:authenticate, :xml => Fixture.response(:authentication))
429
+
430
+ expect(response.hash["ENVELOPE"]["BODY"]).to include("AUTHENTICATE_RESPONSE")
431
+ end
432
+
433
+ it "accepts a block in the block-based interface" do
434
+ client = Savon.client do |globals|
435
+ globals.log false
436
+ globals.wsdl Fixture.wsdl(:authentication)
437
+ globals.endpoint @server.url(:repeat)
438
+ globals.convert_response_tags_to { |tag| tag.snakecase.upcase }
439
+ end
440
+
441
+ response = client.call(:authenticate) do |locals|
442
+ locals.xml Fixture.response(:authentication)
443
+ end
444
+
445
+ expect(response.hash["ENVELOPE"]["BODY"]).to include("AUTHENTICATE_RESPONSE")
446
+ end
447
+ end
448
+
449
+ context "request: message_tag" do
450
+ it "when set, changes the SOAP message tag" do
451
+ response = new_client(:endpoint => @server.url(:repeat)).call(:authenticate, :message_tag => :doAuthenticate)
452
+ expect(response.http.body).to include("<tns:doAuthenticate></tns:doAuthenticate>")
453
+ end
454
+
455
+ it "without it, Savon tries to get the message tag from the WSDL document" do
456
+ response = new_client(:endpoint => @server.url(:repeat)).call(:authenticate)
457
+ expect(response.http.body).to include("<tns:authenticate></tns:authenticate>")
458
+ end
459
+
460
+ it "without the option and a WSDL, Savon defaults to Gyoku to create the name" do
461
+ client = Savon.client(:endpoint => @server.url(:repeat), :namespace => "http://v1.example.com", :log => false)
462
+
463
+ response = client.call(:init_authentication)
464
+ expect(response.http.body).to include("<wsdl:initAuthentication></wsdl:initAuthentication>")
465
+ end
466
+ end
467
+
468
+ context "request: soap_action" do
469
+ it "without it, Savon tries to get the SOAPAction from the WSDL document and falls back to Gyoku" do
470
+ client = new_client(:endpoint => @server.url(:inspect_header),
471
+ :headers => { "Inspect" => "HTTP_SOAPACTION" })
472
+
473
+ response = client.call(:authenticate)
474
+ expect(response.http.body).to eq('"authenticate"')
475
+ end
476
+
477
+ it "when set, changes the SOAPAction HTTP header" do
478
+ client = new_client(:endpoint => @server.url(:inspect_header),
479
+ :headers => { "Inspect" => "HTTP_SOAPACTION" })
480
+
481
+ response = client.call(:authenticate, :soap_action => "doAuthenticate")
482
+ expect(response.http.body).to eq('"doAuthenticate"')
483
+ end
484
+ end
485
+
486
+ context "request :message" do
487
+ it "accepts a Hash which is passed to Gyoku to be converted to XML" do
488
+ response = new_client(:endpoint => @server.url(:repeat)).call(:authenticate, :message => { :user => "luke", :password => "secret" })
489
+ expect(response.http.body).to include("<tns:authenticate><user>luke</user><password>secret</password></tns:authenticate>")
490
+ end
491
+
492
+ it "also accepts a String of raw XML" do
493
+ response = new_client(:endpoint => @server.url(:repeat)).call(:authenticate, :message => "<user>lea</user><password>top-secret</password>")
494
+ expect(response.http.body).to include("<tns:authenticate><user>lea</user><password>top-secret</password></tns:authenticate>")
495
+ end
496
+ end
497
+
498
+ context "request :xml" do
499
+ it "accepts a String of raw XML" do
500
+ response = new_client(:endpoint => @server.url(:repeat)).call(:authenticate, :xml => "<soap>request</soap>")
501
+ expect(response.http.body).to eq("<soap>request</soap>")
502
+ end
503
+ end
504
+
505
+ context "request :advanced_typecasting" do
506
+ it "can be changed to false to disable Nori's advanced typecasting" do
507
+ client = new_client(:endpoint => @server.url(:repeat))
508
+ response = client.call(:authenticate, :xml => Fixture.response(:authentication), :advanced_typecasting => false)
509
+
510
+ expect(response.body[:authenticate_response][:return][:success]).to eq("true")
511
+ end
512
+ end
513
+
514
+ context "request :response_parser" do
515
+ it "instructs Nori to change the response parser" do
516
+ nori = Nori.new(:strip_namespaces => true, :convert_tags_to => lambda { |tag| tag.snakecase.to_sym })
517
+ Nori.expects(:new).with { |options| options[:parser] == :nokogiri }.returns(nori)
518
+
519
+ client = new_client(:endpoint => @server.url(:repeat))
520
+ response = client.call(:authenticate, :xml => Fixture.response(:authentication), :response_parser => :nokogiri)
521
+
522
+ expect(response.body).to_not be_empty
523
+ end
524
+ end
525
+
526
+ def new_client(globals = {}, &block)
527
+ globals = { :wsdl => Fixture.wsdl(:authentication), :log => false }.merge(globals)
528
+ Savon.client(globals, &block)
529
+ end
530
+
531
+ def new_client_without_wsdl(globals = {}, &block)
532
+ globals = { :log => false }.merge(globals)
533
+ Savon.client(globals, &block)
534
+ end
535
+
536
+ end