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.
- data/CHANGELOG.md +119 -104
- data/README.md +12 -11
- data/Rakefile +0 -6
- data/lib/savon.rb +16 -14
- data/lib/savon/block_interface.rb +26 -0
- data/lib/savon/builder.rb +142 -0
- data/lib/savon/client.rb +36 -135
- data/lib/savon/header.rb +42 -0
- data/lib/savon/http_error.rb +27 -0
- data/lib/savon/log_message.rb +23 -25
- data/lib/savon/message.rb +35 -0
- data/lib/savon/mock.rb +5 -0
- data/lib/savon/mock/expectation.rb +70 -0
- data/lib/savon/mock/spec_helper.rb +62 -0
- data/lib/savon/model.rb +39 -61
- data/lib/savon/operation.rb +62 -0
- data/lib/savon/options.rb +265 -0
- data/lib/savon/qualified_message.rb +49 -0
- data/lib/savon/request.rb +92 -0
- data/lib/savon/response.rb +97 -0
- data/lib/savon/soap_fault.rb +40 -0
- data/lib/savon/version.rb +1 -1
- data/savon.gemspec +10 -8
- data/spec/integration/options_spec.rb +536 -0
- data/spec/integration/request_spec.rb +31 -16
- data/spec/integration/support/application.rb +80 -0
- data/spec/integration/support/server.rb +84 -0
- data/spec/savon/builder_spec.rb +81 -0
- data/spec/savon/client_spec.rb +90 -488
- data/spec/savon/http_error_spec.rb +49 -0
- data/spec/savon/log_message_spec.rb +33 -0
- data/spec/savon/mock_spec.rb +127 -0
- data/spec/savon/model_spec.rb +110 -99
- data/spec/savon/observers_spec.rb +92 -0
- data/spec/savon/operation_spec.rb +49 -0
- data/spec/savon/request_spec.rb +145 -0
- data/spec/savon/{soap/response_spec.rb → response_spec.rb} +22 -59
- data/spec/savon/soap_fault_spec.rb +94 -0
- data/spec/spec_helper.rb +5 -3
- data/spec/support/fixture.rb +5 -1
- metadata +202 -197
- data/lib/savon/config.rb +0 -46
- data/lib/savon/error.rb +0 -6
- data/lib/savon/hooks/group.rb +0 -68
- data/lib/savon/hooks/hook.rb +0 -61
- data/lib/savon/http/error.rb +0 -42
- data/lib/savon/logger.rb +0 -39
- data/lib/savon/null_logger.rb +0 -10
- data/lib/savon/soap.rb +0 -21
- data/lib/savon/soap/fault.rb +0 -59
- data/lib/savon/soap/invalid_response_error.rb +0 -13
- data/lib/savon/soap/request.rb +0 -86
- data/lib/savon/soap/request_builder.rb +0 -205
- data/lib/savon/soap/response.rb +0 -117
- data/lib/savon/soap/xml.rb +0 -257
- data/spec/savon/config_spec.rb +0 -38
- data/spec/savon/hooks/group_spec.rb +0 -71
- data/spec/savon/hooks/hook_spec.rb +0 -16
- data/spec/savon/http/error_spec.rb +0 -52
- data/spec/savon/logger_spec.rb +0 -51
- data/spec/savon/savon_spec.rb +0 -33
- data/spec/savon/soap/fault_spec.rb +0 -89
- data/spec/savon/soap/request_builder_spec.rb +0 -207
- data/spec/savon/soap/request_spec.rb +0 -112
- data/spec/savon/soap/xml_spec.rb +0 -357
- 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
|
data/lib/savon/version.rb
CHANGED
data/savon.gemspec
CHANGED
@@ -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", "~>
|
19
|
-
s.add_dependency "httpi", "~>
|
20
|
-
s.add_dependency "wasabi", "~>
|
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.
|
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 "
|
28
|
-
s.add_development_dependency "
|
29
|
-
|
30
|
-
s.add_development_dependency "
|
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
|