markdillon-restclient 1.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.
@@ -0,0 +1,20 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = "restclient"
3
+ s.version = "1.0"
4
+ s.summary = "Simple REST client for Ruby, inspired by microframework syntax for specifying actions."
5
+ s.description = "A simple REST client for Ruby, inspired by the Sinatra microframework style of specifying actions: get, put, post, delete."
6
+ s.author = "Adam Wiggins"
7
+ s.email = "adam@heroku.com"
8
+ s.homepage = "http://rest-client.heroku.com/"
9
+ s.has_rdoc = true
10
+ s.platform = Gem::Platform::RUBY
11
+ s.files = %w(Rakefile README.rdoc restclient.gemspec
12
+ lib/restclient.rb
13
+ lib/restclient/request.rb lib/restclient/response.rb
14
+ lib/restclient/exceptions.rb lib/restclient/resource.rb
15
+ spec/base.rb spec/request_spec.rb spec/response_spec.rb
16
+ spec/exceptions_spec.rb spec/resource_spec.rb spec/restclient_spec.rb
17
+ bin/restclient)
18
+ s.executables = ['restclient']
19
+ s.require_path = "lib"
20
+ end
data/spec/base.rb ADDED
@@ -0,0 +1,4 @@
1
+ require 'rubygems'
2
+ require 'spec'
3
+
4
+ require File.dirname(__FILE__) + '/../lib/restclient'
@@ -0,0 +1,54 @@
1
+ require File.dirname(__FILE__) + '/base'
2
+
3
+ describe RestClient::Exception do
4
+ it "sets the exception message to ErrorMessage" do
5
+ RestClient::ResourceNotFound.new.message.should == 'Resource not found'
6
+ end
7
+
8
+ it "contains exceptions in RestClient" do
9
+ RestClient::Unauthorized.new.should be_a_kind_of(RestClient::Exception)
10
+ RestClient::ServerBrokeConnection.new.should be_a_kind_of(RestClient::Exception)
11
+ end
12
+ end
13
+
14
+ describe RestClient::RequestFailed do
15
+ it "stores the http response on the exception" do
16
+ begin
17
+ raise RestClient::RequestFailed, :response
18
+ rescue RestClient::RequestFailed => e
19
+ e.response.should == :response
20
+ end
21
+ end
22
+
23
+ it "http_code convenience method for fetching the code as an integer" do
24
+ RestClient::RequestFailed.new(mock('res', :code => '502')).http_code.should == 502
25
+ end
26
+
27
+ it "shows the status code in the message" do
28
+ RestClient::RequestFailed.new(mock('res', :code => '502')).to_s.should match(/502/)
29
+ end
30
+ end
31
+
32
+ describe RestClient::ResourceNotFound do
33
+ it "also has the http response attached" do
34
+ begin
35
+ raise RestClient::ResourceNotFound, :response
36
+ rescue RestClient::ResourceNotFound => e
37
+ e.response.should == :response
38
+ end
39
+ end
40
+ end
41
+
42
+ describe "backwards compatibility" do
43
+ it "alias RestClient::Request::Redirect to RestClient::Redirect" do
44
+ RestClient::Request::Redirect.should == RestClient::Redirect
45
+ end
46
+
47
+ it "alias RestClient::Request::Unauthorized to RestClient::Unauthorized" do
48
+ RestClient::Request::Unauthorized.should == RestClient::Unauthorized
49
+ end
50
+
51
+ it "alias RestClient::Request::RequestFailed to RestClient::RequestFailed" do
52
+ RestClient::Request::RequestFailed.should == RestClient::RequestFailed
53
+ end
54
+ end
@@ -0,0 +1,456 @@
1
+ require File.dirname(__FILE__) + '/base'
2
+
3
+ describe RestClient::Request do
4
+ before do
5
+ @request = RestClient::Request.new(:method => :put, :url => 'http://some/resource', :payload => 'payload')
6
+
7
+ @uri = mock("uri")
8
+ @uri.stub!(:request_uri).and_return('/resource')
9
+ @uri.stub!(:host).and_return('some')
10
+ @uri.stub!(:port).and_return(80)
11
+
12
+ @net = mock("net::http base")
13
+ @http = mock("net::http connection")
14
+ Net::HTTP.stub!(:new).and_return(@net)
15
+ @net.stub!(:start).and_yield(@http)
16
+ @net.stub!(:use_ssl=)
17
+ @net.stub!(:verify_mode=)
18
+ end
19
+
20
+ it "requests xml mimetype" do
21
+ @request.default_headers[:accept].should == 'application/xml'
22
+ end
23
+
24
+ it "decodes an uncompressed result body by passing it straight through" do
25
+ @request.decode(nil, 'xyz').should == 'xyz'
26
+ end
27
+
28
+ it "decodes a gzip body" do
29
+ @request.decode('gzip', "\037\213\b\b\006'\252H\000\003t\000\313T\317UH\257\312,HM\341\002\000G\242(\r\v\000\000\000").should == "i'm gziped\n"
30
+ end
31
+
32
+ it "ingores gzip for empty bodies" do
33
+ @request.decode('gzip', '').should be_empty
34
+ end
35
+
36
+ it "decodes a deflated body" do
37
+ @request.decode('deflate', "x\234+\316\317MUHIM\313I,IMQ(I\255(\001\000A\223\006\363").should == "some deflated text"
38
+ end
39
+
40
+ it "processes a successful result" do
41
+ res = mock("result")
42
+ res.stub!(:code).and_return("200")
43
+ res.stub!(:body).and_return('body')
44
+ res.stub!(:[]).with('content-encoding').and_return(nil)
45
+ @request.process_result(res).should == 'body'
46
+ end
47
+
48
+ it "doesn't classify successful requests as failed" do
49
+ 203.upto(206) do |code|
50
+ res = mock("result")
51
+ res.stub!(:code).and_return(code.to_s)
52
+ res.stub!(:body).and_return("")
53
+ res.stub!(:[]).with('content-encoding').and_return(nil)
54
+ @request.process_result(res).should be_empty
55
+ end
56
+ end
57
+
58
+ it "parses a url into a URI object" do
59
+ URI.should_receive(:parse).with('http://example.com/resource')
60
+ @request.parse_url('http://example.com/resource')
61
+ end
62
+
63
+ it "adds http:// to the front of resources specified in the syntax example.com/resource" do
64
+ URI.should_receive(:parse).with('http://example.com/resource')
65
+ @request.parse_url('example.com/resource')
66
+ end
67
+
68
+ it "extracts the username and password when parsing http://user:password@example.com/" do
69
+ URI.stub!(:parse).and_return(mock('uri', :user => 'joe', :password => 'pass1'))
70
+ @request.parse_url_with_auth('http://joe:pass1@example.com/resource')
71
+ @request.user.should == 'joe'
72
+ @request.password.should == 'pass1'
73
+ end
74
+
75
+ it "doesn't overwrite user and password (which may have already been set by the Resource constructor) if there is no user/password in the url" do
76
+ URI.stub!(:parse).and_return(mock('uri', :user => nil, :password => nil))
77
+ @request = RestClient::Request.new(:method => 'get', :url => 'example.com', :user => 'beth', :password => 'pass2')
78
+ @request.parse_url_with_auth('http://example.com/resource')
79
+ @request.user.should == 'beth'
80
+ @request.password.should == 'pass2'
81
+ end
82
+
83
+ it "correctly formats cookies provided to the constructor" do
84
+ URI.stub!(:parse).and_return(mock('uri', :user => nil, :password => nil))
85
+ @request = RestClient::Request.new(:method => 'get', :url => 'example.com', :cookies => {:session_id => '1' })
86
+ @request.should_receive(:default_headers).and_return({'foo' => 'bar'})
87
+ headers = @request.make_headers({}).should == { 'Foo' => 'bar', 'Cookie' => 'session_id=1'}
88
+ end
89
+
90
+ it "determines the Net::HTTP class to instantiate by the method name" do
91
+ @request.net_http_request_class(:put).should == Net::HTTP::Put
92
+ end
93
+
94
+ it "merges user headers with the default headers" do
95
+ @request.should_receive(:default_headers).and_return({ '1' => '2' })
96
+ @request.make_headers('3' => '4').should == { '1' => '2', '3' => '4' }
97
+ end
98
+
99
+ it "prefers the user header when the same header exists in the defaults" do
100
+ @request.should_receive(:default_headers).and_return({ '1' => '2' })
101
+ @request.make_headers('1' => '3').should == { '1' => '3' }
102
+ end
103
+
104
+ it "converts header symbols from :content_type to 'Content-type'" do
105
+ @request.should_receive(:default_headers).and_return({})
106
+ @request.make_headers(:content_type => 'abc').should == { 'Content-type' => 'abc' }
107
+ end
108
+
109
+ it "converts header values to strings" do
110
+ @request.make_headers('A' => 1)['A'].should == '1'
111
+ end
112
+
113
+ it "executes by constructing the Net::HTTP object, headers, and payload and calling transmit" do
114
+ @request.should_receive(:parse_url_with_auth).with('http://some/resource').and_return(@uri)
115
+ klass = mock("net:http class")
116
+ @request.should_receive(:net_http_request_class).with(:put).and_return(klass)
117
+ klass.should_receive(:new).and_return('result')
118
+ @request.should_receive(:transmit).with(@uri, 'result', 'payload')
119
+ @request.execute_inner
120
+ end
121
+
122
+ it "transmits the request with Net::HTTP" do
123
+ @http.should_receive(:request).with('req', 'payload')
124
+ @request.should_receive(:process_result)
125
+ @request.should_receive(:response_log)
126
+ @request.transmit(@uri, 'req', 'payload')
127
+ end
128
+
129
+ it "uses SSL when the URI refers to a https address" do
130
+ @uri.stub!(:is_a?).with(URI::HTTPS).and_return(true)
131
+ @net.should_receive(:use_ssl=).with(true)
132
+ @http.stub!(:request)
133
+ @request.stub!(:process_result)
134
+ @request.stub!(:response_log)
135
+ @request.transmit(@uri, 'req', 'payload')
136
+ end
137
+
138
+ it "sends nil payloads" do
139
+ @http.should_receive(:request).with('req', nil)
140
+ @request.should_receive(:process_result)
141
+ @request.stub!(:response_log)
142
+ @request.transmit(@uri, 'req', nil)
143
+ end
144
+
145
+ it "passes non-hash payloads straight through" do
146
+ @request.process_payload("x").should == "x"
147
+ end
148
+
149
+ it "converts a hash payload to urlencoded data" do
150
+ @request.process_payload(:a => 'b c+d').should == "a=b%20c%2Bd"
151
+ end
152
+
153
+ it "accepts nested hashes in payload" do
154
+ payload = @request.process_payload(:user => { :name => 'joe', :location => { :country => 'USA', :state => 'CA' }})
155
+ payload.should include('user[name]=joe')
156
+ payload.should include('user[location][country]=USA')
157
+ payload.should include('user[location][state]=CA')
158
+ end
159
+
160
+ it "set urlencoded content_type header on hash payloads" do
161
+ @request.process_payload(:a => 1)
162
+ @request.headers[:content_type].should == 'application/x-www-form-urlencoded'
163
+ end
164
+
165
+ it "sets up the credentials prior to the request" do
166
+ @http.stub!(:request)
167
+ @request.stub!(:process_result)
168
+ @request.stub!(:response_log)
169
+
170
+ @request.stub!(:user).and_return('joe')
171
+ @request.stub!(:password).and_return('mypass')
172
+ @request.should_receive(:setup_credentials).with('req')
173
+
174
+ @request.transmit(@uri, 'req', nil)
175
+ end
176
+
177
+ it "does not attempt to send any credentials if user is nil" do
178
+ @request.stub!(:user).and_return(nil)
179
+ req = mock("request")
180
+ req.should_not_receive(:basic_auth)
181
+ @request.setup_credentials(req)
182
+ end
183
+
184
+ it "setup credentials when there's a user" do
185
+ @request.stub!(:user).and_return('joe')
186
+ @request.stub!(:password).and_return('mypass')
187
+ req = mock("request")
188
+ req.should_receive(:basic_auth).with('joe', 'mypass')
189
+ @request.setup_credentials(req)
190
+ end
191
+
192
+ it "catches EOFError and shows the more informative ServerBrokeConnection" do
193
+ @http.stub!(:request).and_raise(EOFError)
194
+ lambda { @request.transmit(@uri, 'req', nil) }.should raise_error(RestClient::ServerBrokeConnection)
195
+ end
196
+
197
+ it "execute calls execute_inner" do
198
+ @request.should_receive(:execute_inner)
199
+ @request.execute
200
+ end
201
+
202
+ it "class method execute wraps constructor" do
203
+ req = mock("rest request")
204
+ RestClient::Request.should_receive(:new).with(1 => 2).and_return(req)
205
+ req.should_receive(:execute)
206
+ RestClient::Request.execute(1 => 2)
207
+ end
208
+
209
+ it "raises a Redirect with the new location when the response is in the 30x range" do
210
+ res = mock('response', :code => '301', :header => { 'Location' => 'http://new/resource' })
211
+ lambda { @request.process_result(res) }.should raise_error(RestClient::Redirect) { |e| e.url.should == 'http://new/resource'}
212
+ end
213
+
214
+ it "handles redirects with relative paths" do
215
+ res = mock('response', :code => '301', :header => { 'Location' => 'index' })
216
+ lambda { @request.process_result(res) }.should raise_error(RestClient::Redirect) { |e| e.url.should == 'http://some/index' }
217
+ end
218
+
219
+ it "handles redirects with absolute paths" do
220
+ @request.instance_variable_set('@url', 'http://some/place/else')
221
+ res = mock('response', :code => '301', :header => { 'Location' => '/index' })
222
+ lambda { @request.process_result(res) }.should raise_error(RestClient::Redirect) { |e| e.url.should == 'http://some/index' }
223
+ end
224
+
225
+ it "raises Unauthorized when the response is 401" do
226
+ res = mock('response', :code => '401')
227
+ lambda { @request.process_result(res) }.should raise_error(RestClient::Unauthorized)
228
+ end
229
+
230
+ it "raises ResourceNotFound when the response is 404" do
231
+ res = mock('response', :code => '404')
232
+ lambda { @request.process_result(res) }.should raise_error(RestClient::ResourceNotFound)
233
+ end
234
+
235
+ it "raises RequestFailed otherwise" do
236
+ res = mock('response', :code => '500')
237
+ lambda { @request.process_result(res) }.should raise_error(RestClient::RequestFailed)
238
+ end
239
+
240
+ it "creates a proxy class if a proxy url is given" do
241
+ RestClient.stub!(:proxy).and_return("http://example.com/")
242
+ @request.net_http_class.should include(Net::HTTP::ProxyDelta)
243
+ end
244
+
245
+ it "creates a non-proxy class if a proxy url is not given" do
246
+ @request.net_http_class.should_not include(Net::HTTP::ProxyDelta)
247
+ end
248
+
249
+ it "logs a get request" do
250
+ RestClient::Request.new(:method => :get, :url => 'http://url').request_log.should ==
251
+ 'RestClient.get "http://url"'
252
+ end
253
+
254
+ it "logs a post request with a small payload" do
255
+ RestClient::Request.new(:method => :post, :url => 'http://url', :payload => 'foo').request_log.should ==
256
+ 'RestClient.post "http://url", "foo"'
257
+ end
258
+
259
+ it "logs a post request with a large payload" do
260
+ RestClient::Request.new(:method => :post, :url => 'http://url', :payload => ('x' * 1000)).request_log.should ==
261
+ 'RestClient.post "http://url", "(1000 byte payload)"'
262
+ end
263
+
264
+ it "logs input headers as a hash" do
265
+ RestClient::Request.new(:method => :get, :url => 'http://url', :headers => { :accept => 'text/plain' }).request_log.should ==
266
+ 'RestClient.get "http://url", :accept=>"text/plain"'
267
+ end
268
+
269
+ it "logs a response including the status code, content type, and result body size in bytes" do
270
+ res = mock('result', :code => '200', :class => Net::HTTPOK, :body => 'abcd')
271
+ res.stub!(:[]).with('Content-type').and_return('text/html')
272
+ @request.response_log(res).should == "# => 200 OK | text/html 4 bytes"
273
+ end
274
+
275
+ it "logs a response with a nil Content-type" do
276
+ res = mock('result', :code => '200', :class => Net::HTTPOK, :body => 'abcd')
277
+ res.stub!(:[]).with('Content-type').and_return(nil)
278
+ @request.response_log(res).should == "# => 200 OK | 4 bytes"
279
+ end
280
+
281
+ it "strips the charset from the response content type" do
282
+ res = mock('result', :code => '200', :class => Net::HTTPOK, :body => 'abcd')
283
+ res.stub!(:[]).with('Content-type').and_return('text/html; charset=utf-8')
284
+ @request.response_log(res).should == "# => 200 OK | text/html 4 bytes"
285
+ end
286
+
287
+ it "displays the log to stdout" do
288
+ RestClient.stub!(:log).and_return('stdout')
289
+ STDOUT.should_receive(:puts).with('xyz')
290
+ @request.display_log('xyz')
291
+ end
292
+
293
+ it "displays the log to stderr" do
294
+ RestClient.stub!(:log).and_return('stderr')
295
+ STDERR.should_receive(:puts).with('xyz')
296
+ @request.display_log('xyz')
297
+ end
298
+
299
+ it "append the log to the requested filename" do
300
+ RestClient.stub!(:log).and_return('/tmp/restclient.log')
301
+ f = mock('file handle')
302
+ File.should_receive(:open).with('/tmp/restclient.log', 'a').and_yield(f)
303
+ f.should_receive(:puts).with('xyz')
304
+ @request.display_log('xyz')
305
+ end
306
+
307
+ it "set read_timeout" do
308
+ @request = RestClient::Request.new(:method => :put, :url => 'http://some/resource', :payload => 'payload', :timeout => 123)
309
+ @http.stub!(:request)
310
+ @request.stub!(:process_result)
311
+ @request.stub!(:response_log)
312
+
313
+ @net.should_receive(:read_timeout=).with(123)
314
+
315
+ @request.transmit(@uri, 'req', nil)
316
+ end
317
+
318
+ it "set open_timeout" do
319
+ @request = RestClient::Request.new(:method => :put, :url => 'http://some/resource', :payload => 'payload', :open_timeout => 123)
320
+ @http.stub!(:request)
321
+ @request.stub!(:process_result)
322
+ @request.stub!(:response_log)
323
+
324
+ @net.should_receive(:open_timeout=).with(123)
325
+
326
+ @request.transmit(@uri, 'req', nil)
327
+ end
328
+
329
+ it "should default to not verifying ssl certificates" do
330
+ @request.verify_ssl.should == false
331
+ end
332
+
333
+ it "should set net.verify_mode to OpenSSL::SSL::VERIFY_NONE if verify_ssl is false" do
334
+ @net.should_receive(:verify_mode=).with(OpenSSL::SSL::VERIFY_NONE)
335
+ @http.stub!(:request)
336
+ @request.stub!(:process_result)
337
+ @request.stub!(:response_log)
338
+ @request.transmit(@uri, 'req', 'payload')
339
+ end
340
+
341
+ it "should not set net.verify_mode to OpenSSL::SSL::VERIFY_NONE if verify_ssl is true" do
342
+ @request = RestClient::Request.new(:method => :put, :url => 'https://some/resource', :payload => 'payload', :verify_ssl => true)
343
+ @net.should_not_receive(:verify_mode=).with(OpenSSL::SSL::VERIFY_NONE)
344
+ @http.stub!(:request)
345
+ @request.stub!(:process_result)
346
+ @request.stub!(:response_log)
347
+ @request.transmit(@uri, 'req', 'payload')
348
+ end
349
+
350
+ it "should set net.verify_mode to the passed value if verify_ssl is an OpenSSL constant" do
351
+ mode = OpenSSL::SSL::VERIFY_PEER |
352
+ OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT
353
+ @request = RestClient::Request.new( :method => :put,
354
+ :url => 'https://some/resource',
355
+ :payload => 'payload',
356
+ :verify_ssl => mode )
357
+ @net.should_receive(:verify_mode=).with(mode)
358
+ @http.stub!(:request)
359
+ @request.stub!(:process_result)
360
+ @request.stub!(:response_log)
361
+ @request.transmit(@uri, 'req', 'payload')
362
+ end
363
+
364
+ it "should default to not having an ssl_client_cert" do
365
+ @request.ssl_client_cert.should be(nil)
366
+ end
367
+
368
+ it "should set the ssl_client_cert if provided" do
369
+ @request = RestClient::Request.new(
370
+ :method => :put,
371
+ :url => 'https://some/resource',
372
+ :payload => 'payload',
373
+ :ssl_client_cert => "whatsupdoc!"
374
+ )
375
+ @net.should_receive(:cert=).with("whatsupdoc!")
376
+ @http.stub!(:request)
377
+ @request.stub!(:process_result)
378
+ @request.stub!(:response_log)
379
+ @request.transmit(@uri, 'req', 'payload')
380
+ end
381
+
382
+ it "should not set the ssl_client_cert if it is not provided" do
383
+ @request = RestClient::Request.new(
384
+ :method => :put,
385
+ :url => 'https://some/resource',
386
+ :payload => 'payload'
387
+ )
388
+ @net.should_not_receive(:cert=).with("whatsupdoc!")
389
+ @http.stub!(:request)
390
+ @request.stub!(:process_result)
391
+ @request.stub!(:response_log)
392
+ @request.transmit(@uri, 'req', 'payload')
393
+ end
394
+
395
+ it "should default to not having an ssl_client_key" do
396
+ @request.ssl_client_key.should be(nil)
397
+ end
398
+
399
+ it "should set the ssl_client_key if provided" do
400
+ @request = RestClient::Request.new(
401
+ :method => :put,
402
+ :url => 'https://some/resource',
403
+ :payload => 'payload',
404
+ :ssl_client_key => "whatsupdoc!"
405
+ )
406
+ @net.should_receive(:key=).with("whatsupdoc!")
407
+ @http.stub!(:request)
408
+ @request.stub!(:process_result)
409
+ @request.stub!(:response_log)
410
+ @request.transmit(@uri, 'req', 'payload')
411
+ end
412
+
413
+ it "should not set the ssl_client_key if it is not provided" do
414
+ @request = RestClient::Request.new(
415
+ :method => :put,
416
+ :url => 'https://some/resource',
417
+ :payload => 'payload'
418
+ )
419
+ @net.should_not_receive(:key=).with("whatsupdoc!")
420
+ @http.stub!(:request)
421
+ @request.stub!(:process_result)
422
+ @request.stub!(:response_log)
423
+ @request.transmit(@uri, 'req', 'payload')
424
+ end
425
+
426
+ it "should default to not having an ssl_ca_file" do
427
+ @request.ssl_ca_file.should be(nil)
428
+ end
429
+
430
+ it "should set the ssl_ca_file if provided" do
431
+ @request = RestClient::Request.new(
432
+ :method => :put,
433
+ :url => 'https://some/resource',
434
+ :payload => 'payload',
435
+ :ssl_ca_file => "Certificate Authority File"
436
+ )
437
+ @net.should_receive(:ca_file=).with("Certificate Authority File")
438
+ @http.stub!(:request)
439
+ @request.stub!(:process_result)
440
+ @request.stub!(:response_log)
441
+ @request.transmit(@uri, 'req', 'payload')
442
+ end
443
+
444
+ it "should not set the ssl_ca_file if it is not provided" do
445
+ @request = RestClient::Request.new(
446
+ :method => :put,
447
+ :url => 'https://some/resource',
448
+ :payload => 'payload'
449
+ )
450
+ @net.should_not_receive(:ca_file=).with("Certificate Authority File")
451
+ @http.stub!(:request)
452
+ @request.stub!(:process_result)
453
+ @request.stub!(:response_log)
454
+ @request.transmit(@uri, 'req', 'payload')
455
+ end
456
+ end