paul-resourceful 0.2.3
Sign up to get free protection for your applications and to get access to all the features.
- data/MIT-LICENSE +21 -0
- data/Manifest.txt +34 -0
- data/README.markdown +86 -0
- data/Rakefile +14 -0
- data/lib/resourceful.rb +29 -0
- data/lib/resourceful/authentication_manager.rb +107 -0
- data/lib/resourceful/cache_manager.rb +174 -0
- data/lib/resourceful/header.rb +31 -0
- data/lib/resourceful/http_accessor.rb +85 -0
- data/lib/resourceful/net_http_adapter.rb +60 -0
- data/lib/resourceful/options_interpreter.rb +78 -0
- data/lib/resourceful/request.rb +63 -0
- data/lib/resourceful/resource.rb +266 -0
- data/lib/resourceful/response.rb +175 -0
- data/lib/resourceful/stubbed_resource_proxy.rb +47 -0
- data/lib/resourceful/util.rb +6 -0
- data/lib/resourceful/version.rb +1 -0
- data/resourceful.gemspec +30 -0
- data/spec/acceptance_shared_specs.rb +49 -0
- data/spec/acceptance_spec.rb +408 -0
- data/spec/resourceful/authentication_manager_spec.rb +249 -0
- data/spec/resourceful/cache_manager_spec.rb +211 -0
- data/spec/resourceful/header_spec.rb +38 -0
- data/spec/resourceful/http_accessor_spec.rb +125 -0
- data/spec/resourceful/net_http_adapter_spec.rb +96 -0
- data/spec/resourceful/options_interpreter_spec.rb +94 -0
- data/spec/resourceful/request_spec.rb +186 -0
- data/spec/resourceful/resource_spec.rb +600 -0
- data/spec/resourceful/response_spec.rb +238 -0
- data/spec/resourceful/stubbed_resource_proxy_spec.rb +58 -0
- data/spec/simple_http_server_shared_spec.rb +160 -0
- data/spec/simple_http_server_shared_spec_spec.rb +212 -0
- data/spec/spec.opts +3 -0
- data/spec/spec_helper.rb +14 -0
- metadata +98 -0
@@ -0,0 +1,238 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
require Pathname(__FILE__).dirname + '../spec_helper'
|
3
|
+
|
4
|
+
require 'resourceful/response'
|
5
|
+
|
6
|
+
describe Resourceful::Response do
|
7
|
+
before do
|
8
|
+
@net_http = mock('net_http')
|
9
|
+
Net::HTTP::Get.stub!(:new).and_return(@net_http)
|
10
|
+
@uri = 'http://www.example.com'
|
11
|
+
|
12
|
+
@response = Resourceful::Response.new(@uri, 0, {}, "")
|
13
|
+
end
|
14
|
+
|
15
|
+
describe 'init' do
|
16
|
+
|
17
|
+
it 'should be instantiatable' do
|
18
|
+
@response.should be_instance_of(Resourceful::Response)
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'should take a [uri, code, header, body] array' do
|
22
|
+
r = Resourceful::Response.new(@uri, 200, {}, "")
|
23
|
+
r.code.should == 200
|
24
|
+
r.header.should == {}
|
25
|
+
r.body.should == ""
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'should have a code' do
|
31
|
+
@response.should respond_to(:code)
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'should have a header' do
|
35
|
+
@response.should respond_to(:header)
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'should have header aliased as headers' do
|
39
|
+
@response.should respond_to(:headers)
|
40
|
+
@response.headers.should == @response.header
|
41
|
+
end
|
42
|
+
|
43
|
+
describe "#is_success?" do
|
44
|
+
it 'should be true for 200' do
|
45
|
+
Resourceful::Response.new(@uri, 200, {}, "").is_success?.should == true
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'should be true for any 2xx' do
|
49
|
+
Resourceful::Response.new(@uri, 299, {}, "").is_success?.should == true
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'should not be true for 300' do
|
53
|
+
Resourceful::Response.new(@uri, 300, {}, "").is_success?.should == false
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'should not be true for 199' do
|
57
|
+
Resourceful::Response.new(@uri, 199, {}, "").is_success?.should == false
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
describe '#is_unsuccesful?' do
|
62
|
+
it 'should be true for a 4xx series response code' do
|
63
|
+
Resourceful::Response.new(@uri, 404, {}, "").is_unsuccesful?.should == true
|
64
|
+
end
|
65
|
+
|
66
|
+
it 'should be true for a 5xx series response code' do
|
67
|
+
Resourceful::Response.new(@uri, 500, {}, "").is_unsuccesful?.should == true
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'should be not true for a 2xx series response code' do
|
71
|
+
Resourceful::Response.new(@uri, 200, {}, "").is_unsuccesful?.should == false
|
72
|
+
end
|
73
|
+
|
74
|
+
it 'should be true for a 3xx series response code' do
|
75
|
+
Resourceful::Response.new(@uri, 302, {}, "").is_unsuccesful?.should == true
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
describe '#is_client_error?' do
|
80
|
+
it 'be true for a 4xx series response code' do
|
81
|
+
Resourceful::Response.new(@uri, 404, {}, "").is_client_error?.should == true
|
82
|
+
end
|
83
|
+
|
84
|
+
it 'be false for anything else' do
|
85
|
+
Resourceful::Response.new(@uri, 200, {}, "").is_client_error?.should == false
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
describe '#is_server_error?' do
|
90
|
+
it 'be true for a 5xx series response code' do
|
91
|
+
Resourceful::Response.new(@uri, 500, {}, "").is_server_error?.should == true
|
92
|
+
end
|
93
|
+
|
94
|
+
it 'be false for anything else' do
|
95
|
+
Resourceful::Response.new(@uri, 200, {}, "").is_server_error?.should == false
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
|
100
|
+
it 'should know if it is a redirect' do
|
101
|
+
Resourceful::Response.new(@uri, 301, {}, "").is_redirect?.should == true
|
102
|
+
Resourceful::Response.new(@uri, 302, {}, "").is_redirect?.should == true
|
103
|
+
Resourceful::Response.new(@uri, 303, {}, "").is_redirect?.should == true
|
104
|
+
Resourceful::Response.new(@uri, 307, {}, "").is_redirect?.should == true
|
105
|
+
|
106
|
+
#aliased as was_redirect?
|
107
|
+
Resourceful::Response.new(@uri, 301, {}, "").was_redirect?.should == true
|
108
|
+
end
|
109
|
+
|
110
|
+
it 'should know if it is a permanent redirect' do
|
111
|
+
Resourceful::Response.new(@uri, 301, {}, "").is_permanent_redirect?.should == true
|
112
|
+
end
|
113
|
+
|
114
|
+
it 'should know if it is a temporary redirect' do
|
115
|
+
Resourceful::Response.new(@uri, 303, {}, "").is_temporary_redirect?.should == true
|
116
|
+
end
|
117
|
+
|
118
|
+
it 'should know if its authoritative' do
|
119
|
+
@response.should respond_to(:authoritative?)
|
120
|
+
end
|
121
|
+
|
122
|
+
it 'should allow authoritative to be set' do
|
123
|
+
@response.authoritative = true
|
124
|
+
@response.authoritative?.should be_true
|
125
|
+
end
|
126
|
+
|
127
|
+
it 'should know if it is authorized' do
|
128
|
+
@response.should respond_to(:is_not_authorized?)
|
129
|
+
Resourceful::Response.new(@uri, 200, {}, "").is_not_authorized?.should == false
|
130
|
+
Resourceful::Response.new(@uri, 401, {}, "").is_not_authorized?.should == true
|
131
|
+
end
|
132
|
+
|
133
|
+
describe 'caching and expiration' do
|
134
|
+
before do
|
135
|
+
Time.stub!(:now).and_return(Time.utc(2008,5,15,18,0,1), Time.utc(2008,5,15,20,0,0))
|
136
|
+
|
137
|
+
@response = Resourceful::Response.new(@uri, 0, {'Date' => ['Thu, 15 May 2008 18:00:00 GMT']}, "")
|
138
|
+
@response.request_time = Time.utc(2008,5,15,17,59,59)
|
139
|
+
end
|
140
|
+
|
141
|
+
it 'should know if its #stale?' do
|
142
|
+
@response.should respond_to(:stale?)
|
143
|
+
end
|
144
|
+
|
145
|
+
it 'should be stale if it is expired' do
|
146
|
+
@response.should_receive(:expired?).and_return(true)
|
147
|
+
@response.should be_stale
|
148
|
+
end
|
149
|
+
|
150
|
+
it 'should know if its #expired?' do
|
151
|
+
@response.should respond_to(:expired?)
|
152
|
+
end
|
153
|
+
|
154
|
+
it 'should be expired if Now is after the "Expire" header' do
|
155
|
+
Time.stub!(:now).and_return(Time.utc(2008,5,23,18,0))
|
156
|
+
@response.header['Expire'] = [(Time.now - 60*60).httpdate]
|
157
|
+
|
158
|
+
@response.should be_expired
|
159
|
+
end
|
160
|
+
|
161
|
+
it 'should have a #current_age' do
|
162
|
+
@response.should respond_to(:current_age)
|
163
|
+
end
|
164
|
+
|
165
|
+
it 'should calculate the #current_age' do
|
166
|
+
@response.current_age.should == (2 * 60 * 60 + 2)
|
167
|
+
end
|
168
|
+
|
169
|
+
it 'should know if its #cachable?' do
|
170
|
+
@response.should respond_to(:cachable?)
|
171
|
+
end
|
172
|
+
|
173
|
+
it 'should normally be cachable' do
|
174
|
+
@response.cachable?.should be_true
|
175
|
+
end
|
176
|
+
|
177
|
+
def response_with_header(header = {})
|
178
|
+
Resourceful::Response.new(@uri, 200, header, "")
|
179
|
+
end
|
180
|
+
|
181
|
+
it 'should not be cachable if the vary header has "*"' do
|
182
|
+
r = response_with_header('Vary' => ['*'])
|
183
|
+
r.cachable?.should be_false
|
184
|
+
end
|
185
|
+
|
186
|
+
it 'should not be cachable if the Cache-Control header is set to no-store' do
|
187
|
+
r = response_with_header('Cache-Control' => ['no-store'])
|
188
|
+
r.cachable?.should be_false
|
189
|
+
end
|
190
|
+
|
191
|
+
it 'should be stale if the Cache-Control header is set to must-revalidate' do
|
192
|
+
r = response_with_header('Cache-Control' => ['must-revalidate'])
|
193
|
+
r.should be_stale
|
194
|
+
end
|
195
|
+
|
196
|
+
it 'should be stale if the Cache-Control header is set to no-cache' do
|
197
|
+
r = response_with_header('Cache-Control' => ['no-cache'])
|
198
|
+
r.should be_stale
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
|
203
|
+
describe '#body' do
|
204
|
+
it 'should have a body method' do
|
205
|
+
@response.should respond_to(:body)
|
206
|
+
end
|
207
|
+
|
208
|
+
require 'zlib'
|
209
|
+
['gzip', ' gzip', ' gzip ', 'GZIP', 'gzIP'].each do |gzip|
|
210
|
+
it "ungzip the body if content-encoding header field is #{gzip}" do
|
211
|
+
compressed_date = StringIO.new.tap do |out|
|
212
|
+
Zlib::GzipWriter.new(out).tap do |zout|
|
213
|
+
zout << "This is a test"
|
214
|
+
zout.close
|
215
|
+
end
|
216
|
+
end.string
|
217
|
+
|
218
|
+
@response = Resourceful::Response.new(@uri, 0, {'Content-Encoding' => [gzip]}, compressed_date)
|
219
|
+
|
220
|
+
@response.body.should == "This is a test"
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
it 'should leave body unmolested if Content-Encoding missing' do
|
225
|
+
@response = Resourceful::Response.new(@uri, 0, {}, "This is a test")
|
226
|
+
@response.body.should == "This is a test"
|
227
|
+
end
|
228
|
+
|
229
|
+
it 'should raise error if Content-Encoding is not supported' do
|
230
|
+
@response = Resourceful::Response.new(@uri, 0, {'Content-Encoding' => ['broken-identity']}, "This is a test")
|
231
|
+
lambda {
|
232
|
+
@response.body
|
233
|
+
}.should raise_error(Resourceful::UnsupportedContentCoding)
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
end
|
238
|
+
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
require Pathname(__FILE__).dirname + '../spec_helper'
|
3
|
+
|
4
|
+
require 'resourceful/stubbed_resource_proxy'
|
5
|
+
|
6
|
+
describe Resourceful::StubbedResourceProxy, "init" do
|
7
|
+
it 'should require real resource' do
|
8
|
+
lambda{
|
9
|
+
Resourceful::StubbedResourceProxy.new
|
10
|
+
}.should raise_error(ArgumentError)
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'should require canned responses hash' do
|
14
|
+
lambda{
|
15
|
+
Resourceful::StubbedResourceProxy.new(stub('resource'))
|
16
|
+
}.should raise_error(ArgumentError)
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'should be creatable with a Resource and canned responses' do
|
20
|
+
Resourceful::StubbedResourceProxy.new(stub('resource'), {})
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe Resourceful::StubbedResourceProxy do
|
25
|
+
before do
|
26
|
+
@resource = stub('resource', :effective_uri => "http://test.invalid/foo")
|
27
|
+
@stubbed_resource = Resourceful::StubbedResourceProxy.
|
28
|
+
new(@resource, [{:mime_type => 'application/xml',
|
29
|
+
:body => '<thing>1</thing>'}])
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'should return canned response body for matching requests' do
|
33
|
+
resp = @stubbed_resource.get
|
34
|
+
resp.body.should == '<thing>1</thing>'
|
35
|
+
resp['content-type'].should == 'application/xml'
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'should pass #get() through to base resource if no matching canned response is defined' do
|
39
|
+
@resource.should_receive(:get)
|
40
|
+
@stubbed_resource.get(:accept => 'application/unknown')
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'should pass #post() through to base resource if no canned response is defined' do
|
44
|
+
@resource.should_receive(:post)
|
45
|
+
@stubbed_resource.post
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'should pass #put() through to base resource if no canned response is defined' do
|
49
|
+
@resource.should_receive(:put)
|
50
|
+
@stubbed_resource.put
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'should pass #effective_uri() through to base resource if no canned response is defined' do
|
54
|
+
@resource.should_receive(:effective_uri)
|
55
|
+
@stubbed_resource.effective_uri
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
@@ -0,0 +1,160 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
# this sets up a very simple http server using thin to be used in specs.
|
4
|
+
SimpleGet = lambda do |env|
|
5
|
+
body = ["Hello, world!"]
|
6
|
+
[ 200, {'Content-Type' => 'text/plain', 'Content-Length' => body.join.size.to_s}, body ]
|
7
|
+
end unless defined? SimpleGet
|
8
|
+
|
9
|
+
SimplePost = lambda do |env|
|
10
|
+
body = [env['rack.input'].string]
|
11
|
+
[ 201, {'Content-Type' => 'text/plain', 'Content-Length' => body.join.size.to_s}, body ]
|
12
|
+
end unless defined? SimplePost
|
13
|
+
|
14
|
+
SimplePut = lambda do |env|
|
15
|
+
body = [env['rack.input'].string]
|
16
|
+
[ 200, {'Content-Type' => 'text/plain', 'Content-Length' => body.join.size.to_s}, body ]
|
17
|
+
end unless defined? SimplePut
|
18
|
+
|
19
|
+
SimpleDel = lambda do |env|
|
20
|
+
body = ["KABOOM!"]
|
21
|
+
[ 200, {'Content-Type' => 'text/plain', 'Content-Length' => body.join.size.to_s}, body ]
|
22
|
+
end unless defined? SimpleDel
|
23
|
+
|
24
|
+
# has the method used in the body of the response
|
25
|
+
MethodResponder = lambda do |env|
|
26
|
+
body = [env['REQUEST_METHOD']]
|
27
|
+
[ 200, {'Content-Type' => 'text/plain', 'Content-Length' => body.join.size.to_s}, body ]
|
28
|
+
end unless defined? MethodResponder
|
29
|
+
|
30
|
+
# has a response code of whatever it was given in the url /code/{123}
|
31
|
+
CodeResponder = lambda do |env|
|
32
|
+
code = env['PATH_INFO'] =~ /([\d]+)/ ? Integer($1) : 404
|
33
|
+
body = [code.to_s]
|
34
|
+
|
35
|
+
[ code, {'Content-Type' => 'text/plain', 'Content-Length' => body.join.size.to_s}, body ]
|
36
|
+
end unless defined? CodeResponder
|
37
|
+
|
38
|
+
# YAML-parses the query string (expected hash) and sets the header to that
|
39
|
+
HeaderResponder = lambda do |env|
|
40
|
+
header = YAML.load(URI.unescape(env['QUERY_STRING']))
|
41
|
+
body = [header.inspect]
|
42
|
+
|
43
|
+
header.merge!({
|
44
|
+
'Content-Type' => 'text/plain',
|
45
|
+
'Content-Length' => body.join.size.to_s
|
46
|
+
})
|
47
|
+
|
48
|
+
[ 200, header, body ]
|
49
|
+
end unless defined? HeaderResponder
|
50
|
+
|
51
|
+
# Echos the request header in the response body
|
52
|
+
EchoHeaderResponder = lambda do |env|
|
53
|
+
body = [env.inspect]
|
54
|
+
|
55
|
+
header = {
|
56
|
+
'Content-Type' => 'text/plain',
|
57
|
+
'Content-Length' => body.join.size.to_s
|
58
|
+
}
|
59
|
+
|
60
|
+
[ 200, header, body ]
|
61
|
+
end unless defined? EchoHeaderResponder
|
62
|
+
|
63
|
+
# redirect. /redirect/{301|302}?{url}
|
64
|
+
Redirector = lambda do |env|
|
65
|
+
code = env['PATH_INFO'] =~ /([\d]+)/ ? Integer($1) : 404
|
66
|
+
location = env['QUERY_STRING']
|
67
|
+
body = [location]
|
68
|
+
|
69
|
+
[ code, {'Content-Type' => 'text/plain', 'Location' => location, 'Content-Length' => body.join.size.to_s}, body ]
|
70
|
+
end unless defined? Redirector
|
71
|
+
|
72
|
+
# Returns 304 if 'If-Modified-Since' is after given mod time
|
73
|
+
ModifiedResponder = lambda do |env|
|
74
|
+
modtime = Time.httpdate(URI.unescape(env['QUERY_STRING']))
|
75
|
+
|
76
|
+
code = 200
|
77
|
+
if env['HTTP_IF_MODIFIED_SINCE']
|
78
|
+
code = 304
|
79
|
+
end
|
80
|
+
body = [modtime.to_s]
|
81
|
+
|
82
|
+
header = {'Content-Type' => 'text/plain',
|
83
|
+
'Content-Length' => body.join.size.to_s,
|
84
|
+
'Last-Modified' => modtime.httpdate,
|
85
|
+
'Cache-Control' => 'must-revalidate'}
|
86
|
+
|
87
|
+
[ code, header, body ]
|
88
|
+
end unless defined? ModifiedResponder
|
89
|
+
|
90
|
+
require 'rubygems'
|
91
|
+
require 'httpauth'
|
92
|
+
AuthorizationResponder = lambda do |env|
|
93
|
+
authtype = env['QUERY_STRING']
|
94
|
+
header = {}
|
95
|
+
if auth_string = env['HTTP_AUTHORIZATION']
|
96
|
+
if authtype == "basic" &&
|
97
|
+
['admin', 'secret'] == HTTPAuth::Basic.unpack_authorization(auth_string)
|
98
|
+
code = 200
|
99
|
+
body = ["Authorized"]
|
100
|
+
elsif authtype == "digest" #&&
|
101
|
+
credentials = HTTPAuth::Digest::Credentials.from_header(auth_string) &&
|
102
|
+
credentials &&
|
103
|
+
credentials.validate(:password => 'secret', :method => 'GET')
|
104
|
+
code = 200
|
105
|
+
body = ["Authorized"]
|
106
|
+
else
|
107
|
+
code = 401
|
108
|
+
body = ["Not Authorized"]
|
109
|
+
end
|
110
|
+
else
|
111
|
+
code = 401
|
112
|
+
body = ["Not Authorized"]
|
113
|
+
if authtype == "basic"
|
114
|
+
header = {'WWW-Authenticate' => HTTPAuth::Basic.pack_challenge('Test Auth')}
|
115
|
+
elsif authtype == "digest"
|
116
|
+
chal = HTTPAuth::Digest::Credentials.new(:realm => 'Test Auth', :qop => 'auth')
|
117
|
+
header = {'WWW-Authenticate' => chal.to_header}
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
[ code, header.merge({'Content-Type' => 'text/plain', 'Content-Length' => body.join.size.to_s}), body ]
|
122
|
+
end unless defined? AuthorizationResponder
|
123
|
+
|
124
|
+
describe 'simple http server', :shared => true do
|
125
|
+
before(:all) do
|
126
|
+
require 'rack'
|
127
|
+
require 'mongrel'
|
128
|
+
|
129
|
+
app = Rack::Builder.new do |env|
|
130
|
+
use Rack::ShowExceptions
|
131
|
+
|
132
|
+
map( '/get' ){ run SimpleGet }
|
133
|
+
map( '/post' ){ run SimplePost }
|
134
|
+
map( '/put' ){ run SimplePut }
|
135
|
+
map( '/delete' ){ run SimpleDel }
|
136
|
+
|
137
|
+
map( '/method' ){ run MethodResponder }
|
138
|
+
map( '/code' ){ run CodeResponder }
|
139
|
+
map( '/redirect' ){ run Redirector }
|
140
|
+
map( '/header' ){ run HeaderResponder }
|
141
|
+
map( '/echo_header' ){ run EchoHeaderResponder }
|
142
|
+
map( '/modified' ){ run ModifiedResponder }
|
143
|
+
map( '/auth' ){ run AuthorizationResponder }
|
144
|
+
end
|
145
|
+
|
146
|
+
#spawn the server in a separate thread
|
147
|
+
@httpd = Thread.new do
|
148
|
+
Rack::Handler::Mongrel.run(app, :Host => '127.0.0.1', :Port => 3000)
|
149
|
+
end
|
150
|
+
#give the server a chance to initialize
|
151
|
+
sleep 0.05
|
152
|
+
end
|
153
|
+
|
154
|
+
after(:all) do
|
155
|
+
# kill the server thread
|
156
|
+
@httpd.exit
|
157
|
+
end
|
158
|
+
|
159
|
+
|
160
|
+
end
|