savon 1.2.0 → 2.0.0

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 (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