resourceful 0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+