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