resourceful 0.2

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,100 @@
1
+ require 'net/http'
2
+ require 'time'
3
+ require 'rubygems'
4
+ require 'facets/kernel/ergo'
5
+
6
+ module Resourceful
7
+ # Exception indicating that the server used a content coding scheme
8
+ # that Resourceful is unable to handle.
9
+ class UnsupportedContentCoding < Exception
10
+ end
11
+
12
+ class Response
13
+ REDIRECT_RESPONSE_CODES = [301,302,303,307]
14
+
15
+ attr_reader :uri, :code, :header, :body, :response_time
16
+ alias headers header
17
+
18
+ attr_accessor :authoritative, :request_time
19
+ alias authoritative? authoritative
20
+
21
+ def initialize(uri, code, header, body)
22
+ @uri, @code, @header, @body = uri, code, header, body
23
+ @response_time = Time.now
24
+ end
25
+
26
+ def is_success?
27
+ @code.in? 200..299
28
+ end
29
+
30
+ def is_redirect?
31
+ @code.in? REDIRECT_RESPONSE_CODES
32
+ end
33
+ alias was_redirect? is_redirect?
34
+
35
+ def is_permanent_redirect?
36
+ @code == 301
37
+ end
38
+
39
+ def is_temporary_redirect?
40
+ is_redirect? and not is_permanent_redirect?
41
+ end
42
+
43
+ def is_not_authorized?
44
+ @code == 401
45
+ end
46
+
47
+ def expired?
48
+ if header['Expire']
49
+ return true if Time.httpdate(header['Expire'].first) < Time.now
50
+ end
51
+
52
+ false
53
+ end
54
+
55
+ def stale?
56
+ return true if expired?
57
+ if header['Cache-Control']
58
+ return true if header['Cache-Control'].include?('must-revalidate')
59
+ return true if header['Cache-Control'].include?('no-cache')
60
+ end
61
+
62
+ false
63
+ end
64
+
65
+ def cachable?
66
+ return false if header['Vary'] and header['Vary'].include?('*')
67
+ return false if header['Cache-Control'] and header['Cache-Control'].include?('no-store')
68
+
69
+ true
70
+ end
71
+
72
+ # Algorithm taken from RCF2616#13.2.3
73
+ def current_age
74
+ age_value = Time.httpdate(header['Age'].first) if header['Age']
75
+ date_value = Time.httpdate(header['Date'].first)
76
+ now = Time.now
77
+
78
+ apparent_age = [0, response_time - date_value].max
79
+ corrected_received_age = [apparent_age, age_value || 0].max
80
+ current_age = corrected_received_age + (response_time - request_time) + (now - response_time)
81
+ end
82
+
83
+ def body
84
+ case header['Content-Encoding'].ergo.first
85
+ when nil
86
+ # body is identity encoded; just return it
87
+ @body
88
+ when /^\s*gzip\s*$/i
89
+ gz_in = Zlib::GzipReader.new(StringIO.new(@body, 'r'))
90
+ @body = gz_in.read
91
+ gz_in.close
92
+ header.delete('Content-Encoding')
93
+ @body
94
+ else
95
+ raise UnsupportedContentCoding, "Resourceful does not support #{header['Content-Encoding'].ergo.first} content coding"
96
+ end
97
+ end
98
+ end
99
+
100
+ end
@@ -0,0 +1,47 @@
1
+ require 'resourceful/resource'
2
+
3
+ module Resourceful
4
+ class StubbedResourceProxy
5
+ def initialize(resource, canned_responses)
6
+ @resource = resource
7
+
8
+ @canned_responses = {}
9
+
10
+ canned_responses.each do |cr|
11
+ mime_type = cr[:mime_type]
12
+ @canned_responses[mime_type] = resp = Net::HTTPOK.new('1.1', '200', 'OK')
13
+ resp['content-type'] = mime_type.to_str
14
+ resp.instance_variable_set(:@read, true)
15
+ resp.instance_variable_set(:@body, cr[:body])
16
+
17
+ end
18
+ end
19
+
20
+ def get_body(*args)
21
+ get(*args).body
22
+ end
23
+
24
+ def get(*args)
25
+ options = args.last.is_a?(Hash) ? args.last : {}
26
+
27
+ if accept = [(options[:accept] || '*/*')].flatten.compact
28
+ accept.each do |mt|
29
+ return canned_response(mt) || next
30
+ end
31
+ @resource.get(*args)
32
+ end
33
+ end
34
+
35
+ def method_missing(method, *args)
36
+ @resource.send(method, *args)
37
+ end
38
+
39
+ protected
40
+
41
+ def canned_response(mime_type)
42
+ mime_type = @canned_responses.keys.first if mime_type == '*/*'
43
+ @canned_responses[mime_type]
44
+ end
45
+
46
+ end
47
+ end
@@ -0,0 +1,6 @@
1
+
2
+ class Object
3
+ def in?(arr)
4
+ arr.include?(self)
5
+ end
6
+ end
@@ -0,0 +1 @@
1
+ RESOURCEFUL_VERSION = '0.2'
@@ -0,0 +1,49 @@
1
+ describe 'redirect', :shared => true do
2
+ it 'should be followed by default on GET' do
3
+ resp = @resource.get
4
+ resp.should be_instance_of(Resourceful::Response)
5
+ resp.code.should == 200
6
+ resp.header['Content-Type'].should == ['text/plain']
7
+ end
8
+
9
+ %w{PUT POST}.each do |method|
10
+ it "should not be followed by default on #{method}" do
11
+ lambda {
12
+ @resource.send(method.downcase.intern, nil, :content_type => 'text/plain' )
13
+ }.should raise_error(Resourceful::UnsuccessfulHttpRequestError)
14
+ end
15
+
16
+ it "should redirect on #{method.to_s.upcase} if the redirection callback returns true" do
17
+ @resource.on_redirect { true }
18
+ resp = @resource.send(method.downcase.intern, nil, :content_type => 'text/plain' )
19
+ resp.code.should == 200
20
+ end
21
+
22
+ it "should not redirect on #{method.to_s.upcase} if the redirection callback returns false" do
23
+ @resource.on_redirect { false }
24
+ lambda {
25
+ @resource.send(method.downcase.intern, nil, :content_type => 'text/plain' )
26
+ }.should raise_error(Resourceful::UnsuccessfulHttpRequestError)
27
+ end
28
+ end
29
+
30
+ it "should not be followed by default on DELETE" do
31
+ lambda {
32
+ @resource.delete
33
+ }.should raise_error(Resourceful::UnsuccessfulHttpRequestError)
34
+ end
35
+
36
+ it "should redirect on DELETE if vthe redirection callback returns true" do
37
+ @resource.on_redirect { true }
38
+ resp = @resource.delete
39
+ resp.code.should == 200
40
+ end
41
+
42
+ it "should not redirect on DELETE if the redirection callback returns false" do
43
+ @resource.on_redirect { false }
44
+ lambda {
45
+ @resource.delete
46
+ }.should raise_error(Resourceful::UnsuccessfulHttpRequestError)
47
+ end
48
+ end
49
+
@@ -0,0 +1,344 @@
1
+ require 'pathname'
2
+ require Pathname(__FILE__).dirname + 'spec_helper'
3
+ require 'resourceful'
4
+
5
+ require Pathname(__FILE__).dirname + 'acceptance_shared_specs'
6
+
7
+
8
+ describe Resourceful do
9
+ it_should_behave_like 'simple http server'
10
+
11
+ describe 'getting a resource' do
12
+ before do
13
+ @accessor = Resourceful::HttpAccessor.new
14
+ end
15
+
16
+ it 'should #get a resource, and return a response object' do
17
+ resource = @accessor.resource('http://localhost:3000/get')
18
+ resp = resource.get
19
+ resp.should be_instance_of(Resourceful::Response)
20
+ resp.code.should == 200
21
+ resp.body.should == 'Hello, world!'
22
+ resp.header.should be_instance_of(Resourceful::Header)
23
+ resp.header['Content-Type'].should == ['text/plain']
24
+ end
25
+
26
+ it 'should #post a resource, and return the response' do
27
+ resource = @accessor.resource('http://localhost:3000/post')
28
+ resp = resource.post('Hello world from POST', :content_type => 'text/plain')
29
+ resp.should be_instance_of(Resourceful::Response)
30
+ resp.code.should == 201
31
+ resp.body.should == 'Hello world from POST'
32
+ resp.header.should be_instance_of(Resourceful::Header)
33
+ resp.header['Content-Type'].should == ['text/plain']
34
+ end
35
+
36
+ it 'should #put a resource, and return the response' do
37
+ resource = @accessor.resource('http://localhost:3000/put')
38
+ resp = resource.put('Hello world from PUT', :content_type => 'text/plain')
39
+ resp.should be_instance_of(Resourceful::Response)
40
+ resp.code.should == 200
41
+ resp.body.should == 'Hello world from PUT'
42
+ resp.header.should be_instance_of(Resourceful::Header)
43
+ resp.header['Content-Type'].should == ['text/plain']
44
+ end
45
+
46
+ it 'should #delete a resource, and return a response' do
47
+ resource = @accessor.resource('http://localhost:3000/delete')
48
+ resp = resource.delete
49
+ resp.should be_instance_of(Resourceful::Response)
50
+ resp.code.should == 200
51
+ resp.body.should == 'KABOOM!'
52
+ resp.header.should be_instance_of(Resourceful::Header)
53
+ resp.header['Content-Type'].should == ['text/plain']
54
+ end
55
+
56
+ describe 'redirecting' do
57
+
58
+ describe 'registering callback' do
59
+ before do
60
+ @resource = @accessor.resource('http://localhost:3000/redirect/301?http://localhost:3000/get')
61
+ @callback = mock('callback')
62
+ @callback.stub!(:call).and_return(true)
63
+
64
+ @resource.on_redirect { @callback.call }
65
+ end
66
+
67
+ it 'should allow a callback to be registered' do
68
+ @resource.should respond_to(:on_redirect)
69
+ end
70
+
71
+ it 'should perform a registered callback on redirect' do
72
+ @callback.should_receive(:call).and_return(true)
73
+ @resource.get
74
+ end
75
+
76
+ it 'should not perform the redirect if the callback returns false' do
77
+ @callback.should_receive(:call).and_return(false)
78
+ lambda {
79
+ @resource.get
80
+ }.should raise_error(Resourceful::UnsuccessfulHttpRequestError)
81
+ end
82
+ end
83
+
84
+ describe 'permanent redirect' do
85
+ before do
86
+ @redirect_code = 301
87
+ @resource = @accessor.resource('http://localhost:3000/redirect/301?http://localhost:3000/get')
88
+ end
89
+
90
+ it_should_behave_like 'redirect'
91
+
92
+ it 'should change the effective uri of the resource' do
93
+ @resource.get
94
+ @resource.effective_uri.should == 'http://localhost:3000/get'
95
+ end
96
+ end
97
+
98
+ describe 'temporary redirect' do
99
+ before do
100
+ @redirect_code = 302
101
+ @resource = @accessor.resource('http://localhost:3000/redirect/302?http://localhost:3000/get')
102
+ end
103
+
104
+ it_should_behave_like 'redirect'
105
+
106
+ it 'should not change the effective uri of the resource' do
107
+ @resource.get
108
+ @resource.effective_uri.should == 'http://localhost:3000/redirect/302?http://localhost:3000/get'
109
+ end
110
+
111
+ describe '303 See Other' do
112
+ before do
113
+ @redirect_code = 303
114
+ @resource = @accessor.resource('http://localhost:3000/redirect/303?http://localhost:3000/method')
115
+ @resource.on_redirect { true }
116
+ end
117
+
118
+ it 'should GET the redirected resource, regardless of the initial method' do
119
+ resp = @resource.delete
120
+ resp.code.should == 200
121
+ resp.body.should == 'GET'
122
+ end
123
+ end
124
+ end
125
+
126
+ end
127
+
128
+ describe 'caching' do
129
+ before do
130
+ @accessor = Resourceful::HttpAccessor.new(:cache_manager => Resourceful::InMemoryCacheManager.new)
131
+ end
132
+
133
+ it 'should use the cached response' do
134
+ resource = @accessor.resource('http://localhost:3000/get')
135
+ resp = resource.get
136
+ resp.authoritative?.should be_true
137
+
138
+ resp2 = resource.get
139
+ resp2.authoritative?.should be_false
140
+
141
+ resp2.should == resp
142
+ end
143
+
144
+ it 'should not store the representation if the server says not to' do
145
+ resource = @accessor.resource('http://localhost:3000/header?{Vary:%20*}')
146
+ resp = resource.get
147
+ resp.authoritative?.should be_true
148
+ resp.should_not be_cachable
149
+
150
+ resp2 = resource.get
151
+ resp2.should_not == resp
152
+ end
153
+
154
+ it 'should use the cached version of the representation if it has not expired' do
155
+ in_an_hour = (Time.now + (60*60)).httpdate
156
+ uri = URI.escape("http://localhost:3000/header?{Expire: \"#{in_an_hour}\"}")
157
+
158
+ resource = @accessor.resource(uri)
159
+ resp = resource.get
160
+ resp.should be_authoritative
161
+
162
+ resp2 = resource.get
163
+ resp2.should_not be_authoritative
164
+ resp2.should == resp
165
+ end
166
+
167
+ it 'should revalidate the cached response if it has expired' do
168
+ an_hour_ago = (Time.now - (60*60)).httpdate
169
+ uri = URI.escape("http://localhost:3000/header?{Expire: \"#{an_hour_ago}\"}")
170
+
171
+ resource = @accessor.resource(uri)
172
+ resp = resource.get
173
+ resp.should be_authoritative
174
+ resp.should be_expired
175
+
176
+ resp2 = resource.get
177
+ resp2.should be_authoritative
178
+ end
179
+
180
+ it 'should provide the cached version if the server responds with a 304 not modified' do
181
+ in_an_hour = (Time.now + (60*60)).httpdate
182
+ uri = URI.escape("http://localhost:3000/modified?#{in_an_hour}")
183
+
184
+ resource = @accessor.resource(uri)
185
+ resp = resource.get
186
+ resp.should be_authoritative
187
+ resp.header['Cache-Control'].should include('must-revalidate')
188
+
189
+ resp2 = resource.get
190
+ resp2.should be_authoritative
191
+ resp2.should == resp
192
+ end
193
+
194
+ describe 'Cache-Control' do
195
+
196
+ it 'should cache anything with "Cache-Control: public"' do
197
+ uri = URI.escape('http://localhost:3000/header?{Cache-Control: public}')
198
+ resource = @accessor.resource(uri)
199
+ resp = resource.get
200
+ resp.authoritative?.should be_true
201
+
202
+ resp2 = resource.get
203
+ resp2.authoritative?.should be_false
204
+
205
+ resp2.should == resp
206
+ end
207
+
208
+ it 'should cache anything with "Cache-Control: private"' do
209
+ uri = URI.escape('http://localhost:3000/header?{Cache-Control: private}')
210
+ resource = @accessor.resource(uri)
211
+ resp = resource.get
212
+ resp.authoritative?.should be_true
213
+
214
+ resp2 = resource.get
215
+ resp2.authoritative?.should be_false
216
+
217
+ resp2.should == resp
218
+ end
219
+
220
+ it 'should cache but revalidate anything with "Cache-Control: no-cache"' do
221
+ uri = URI.escape('http://localhost:3000/header?{Cache-Control: no-cache}')
222
+ resource = @accessor.resource(uri)
223
+ resp = resource.get
224
+ resp.authoritative?.should be_true
225
+
226
+ resp2 = resource.get
227
+ resp2.authoritative?.should be_true
228
+ end
229
+
230
+ it 'should cache but revalidate anything with "Cache-Control: must-revalidate"' do
231
+ uri = URI.escape('http://localhost:3000/header?{Cache-Control: must-revalidate}')
232
+ resource = @accessor.resource(uri)
233
+ resp = resource.get
234
+ resp.authoritative?.should be_true
235
+
236
+ resp2 = resource.get
237
+ resp2.authoritative?.should be_true
238
+ end
239
+
240
+ it 'should not cache anything with "Cache-Control: no-store"' do
241
+ uri = URI.escape('http://localhost:3000/header?{Cache-Control: no-store}')
242
+ resource = @accessor.resource(uri)
243
+ resp = resource.get
244
+ resp.authoritative?.should be_true
245
+
246
+ resp2 = resource.get
247
+ resp2.authoritative?.should be_true
248
+ end
249
+
250
+
251
+ end
252
+
253
+ end
254
+
255
+ describe 'authorization' do
256
+ before do
257
+ @uri = 'http://localhost:3000/auth?basic'
258
+ end
259
+
260
+ it 'should automatically add authorization info to the request if its available'
261
+
262
+ it 'should not authenticate if no auth handlers are set' do
263
+ resource = @accessor.resource(@uri)
264
+ lambda {
265
+ resource.get
266
+ }.should raise_error(Resourceful::UnsuccessfulHttpRequestError)
267
+ end
268
+
269
+ it 'should not authenticate if no valid auth handlers are available' do
270
+ basic_handler = Resourceful::BasicAuthenticator.new('Not Test Auth', 'admin', 'secret')
271
+ @accessor.auth_manager.add_auth_handler(basic_handler)
272
+ resource = @accessor.resource(@uri)
273
+ lambda {
274
+ resource.get
275
+ }.should raise_error(Resourceful::UnsuccessfulHttpRequestError)
276
+ end
277
+
278
+ describe 'basic' do
279
+ before do
280
+ @uri = 'http://localhost:3000/auth?basic'
281
+ end
282
+
283
+ it 'should be able to authenticate basic auth' do
284
+ basic_handler = Resourceful::BasicAuthenticator.new('Test Auth', 'admin', 'secret')
285
+ @accessor.auth_manager.add_auth_handler(basic_handler)
286
+ resource = @accessor.resource(@uri)
287
+ resp = resource.get
288
+
289
+ resp.code.should == 200
290
+ end
291
+
292
+ it 'should not keep trying to authenticate with incorrect credentials' do
293
+ basic_handler = Resourceful::BasicAuthenticator.new('Test Auth', 'admin', 'well-known')
294
+ @accessor.auth_manager.add_auth_handler(basic_handler)
295
+ resource = @accessor.resource(@uri)
296
+
297
+ lambda {
298
+ resource.get
299
+ }.should raise_error(Resourceful::UnsuccessfulHttpRequestError)
300
+ end
301
+
302
+ end
303
+
304
+ describe 'digest' do
305
+ before do
306
+ @uri = 'http://localhost:3000/auth/digest'
307
+ end
308
+
309
+ it 'should be able to authenticate digest auth' do
310
+ pending
311
+ digest_handler = Resourceful::DigestAuthenticator.new('Test Auth', 'admin', 'secret')
312
+ @accessor.auth_manager.add_auth_handler(digest_handler)
313
+ resource = @accessor.resource(@uri)
314
+ resp = resource.get
315
+
316
+ resp.code.should == 200
317
+ end
318
+
319
+ end
320
+
321
+ end
322
+
323
+ describe 'error checking' do
324
+
325
+ it 'should raise InvalidResponse when response code is invalid'
326
+
327
+ describe 'client errors' do
328
+
329
+ it 'should raise when there is one'
330
+
331
+ end
332
+
333
+ describe 'server errors' do
334
+
335
+ it 'should raise when there is one'
336
+
337
+ end
338
+
339
+ end
340
+
341
+ end
342
+
343
+ end
344
+