resourceful 0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,199 @@
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
+ it 'should know if it is a redirect' do
62
+ Resourceful::Response.new(@uri, 301, {}, "").is_redirect?.should == true
63
+ Resourceful::Response.new(@uri, 302, {}, "").is_redirect?.should == true
64
+ Resourceful::Response.new(@uri, 303, {}, "").is_redirect?.should == true
65
+ Resourceful::Response.new(@uri, 307, {}, "").is_redirect?.should == true
66
+
67
+ #aliased as was_redirect?
68
+ Resourceful::Response.new(@uri, 301, {}, "").was_redirect?.should == true
69
+ end
70
+
71
+ it 'should know if it is a permanent redirect' do
72
+ Resourceful::Response.new(@uri, 301, {}, "").is_permanent_redirect?.should == true
73
+ end
74
+
75
+ it 'should know if it is a temporary redirect' do
76
+ Resourceful::Response.new(@uri, 303, {}, "").is_temporary_redirect?.should == true
77
+ end
78
+
79
+ it 'should know if its authoritative' do
80
+ @response.should respond_to(:authoritative?)
81
+ end
82
+
83
+ it 'should allow authoritative to be set' do
84
+ @response.authoritative = true
85
+ @response.authoritative?.should be_true
86
+ end
87
+
88
+ it 'should know if it is authorized' do
89
+ @response.should respond_to(:is_not_authorized?)
90
+ Resourceful::Response.new(@uri, 200, {}, "").is_not_authorized?.should == false
91
+ Resourceful::Response.new(@uri, 401, {}, "").is_not_authorized?.should == true
92
+ end
93
+
94
+ describe 'caching and expiration' do
95
+ before do
96
+ Time.stub!(:now).and_return(Time.utc(2008,5,15,18,0,1), Time.utc(2008,5,15,20,0,0))
97
+
98
+ @response = Resourceful::Response.new(@uri, 0, {'Date' => ['Thu, 15 May 2008 18:00:00 GMT']}, "")
99
+ @response.request_time = Time.utc(2008,5,15,17,59,59)
100
+ end
101
+
102
+ it 'should know if its #stale?' do
103
+ @response.should respond_to(:stale?)
104
+ end
105
+
106
+ it 'should be stale if it is expired' do
107
+ @response.should_receive(:expired?).and_return(true)
108
+ @response.should be_stale
109
+ end
110
+
111
+ it 'should know if its #expired?' do
112
+ @response.should respond_to(:expired?)
113
+ end
114
+
115
+ it 'should be expired if Now is after the "Expire" header' do
116
+ Time.stub!(:now).and_return(Time.utc(2008,5,23,18,0))
117
+ @response.header['Expire'] = [(Time.now - 60*60).httpdate]
118
+
119
+ @response.should be_expired
120
+ end
121
+
122
+ it 'should have a #current_age' do
123
+ @response.should respond_to(:current_age)
124
+ end
125
+
126
+ it 'should calculate the #current_age' do
127
+ @response.current_age.should == (2 * 60 * 60 + 2)
128
+ end
129
+
130
+ it 'should know if its #cachable?' do
131
+ @response.should respond_to(:cachable?)
132
+ end
133
+
134
+ it 'should normally be cachable' do
135
+ @response.cachable?.should be_true
136
+ end
137
+
138
+ def response_with_header(header = {})
139
+ Resourceful::Response.new(@uri, 200, header, "")
140
+ end
141
+
142
+ it 'should not be cachable if the vary header has "*"' do
143
+ r = response_with_header('Vary' => ['*'])
144
+ r.cachable?.should be_false
145
+ end
146
+
147
+ it 'should not be cachable if the Cache-Control header is set to no-store' do
148
+ r = response_with_header('Cache-Control' => ['no-store'])
149
+ r.cachable?.should be_false
150
+ end
151
+
152
+ it 'should be stale if the Cache-Control header is set to must-revalidate' do
153
+ r = response_with_header('Cache-Control' => ['must-revalidate'])
154
+ r.should be_stale
155
+ end
156
+
157
+ it 'should be stale if the Cache-Control header is set to no-cache' do
158
+ r = response_with_header('Cache-Control' => ['no-cache'])
159
+ r.should be_stale
160
+ end
161
+ end
162
+
163
+
164
+ describe '#body' do
165
+ it 'should have a body method' do
166
+ @response.should respond_to(:body)
167
+ end
168
+
169
+ require 'zlib'
170
+ ['gzip', ' gzip', ' gzip ', 'GZIP', 'gzIP'].each do |gzip|
171
+ it "ungzip the body if content-encoding header field is #{gzip}" do
172
+ compressed_date = StringIO.new.tap do |out|
173
+ Zlib::GzipWriter.new(out).tap do |zout|
174
+ zout << "This is a test"
175
+ zout.close
176
+ end
177
+ end.string
178
+
179
+ @response = Resourceful::Response.new(@uri, 0, {'Content-Encoding' => [gzip]}, compressed_date)
180
+
181
+ @response.body.should == "This is a test"
182
+ end
183
+ end
184
+
185
+ it 'should leave body unmolested if Content-Encoding missing' do
186
+ @response = Resourceful::Response.new(@uri, 0, {}, "This is a test")
187
+ @response.body.should == "This is a test"
188
+ end
189
+
190
+ it 'should raise error if Content-Encoding is not supported' do
191
+ @response = Resourceful::Response.new(@uri, 0, {'Content-Encoding' => ['broken-identity']}, "This is a test")
192
+ lambda {
193
+ @response.body
194
+ }.should raise_error(Resourceful::UnsupportedContentCoding)
195
+ end
196
+ end
197
+
198
+ end
199
+
@@ -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,151 @@
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 quesy 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
+ # redirect. /redirect/{301|302}?{url}
52
+ Redirector = lambda do |env|
53
+ code = env['PATH_INFO'] =~ /([\d]+)/ ? Integer($1) : 404
54
+ location = env['QUERY_STRING']
55
+ body = [location]
56
+
57
+ [ code, {'Content-Type' => 'text/plain', 'Location' => location, 'Content-Length' => body.join.size.to_s}, body ]
58
+ end unless defined? Redirector
59
+
60
+ # Returns 304 if 'If-Modified-Since' is after given mod time
61
+ ModifiedResponder = lambda do |env|
62
+ modtime = Time.httpdate(URI.unescape(env['QUERY_STRING']))
63
+
64
+ code = 200
65
+ if env['HTTP_IF_MODIFIED_SINCE']
66
+ code = 304
67
+ end
68
+ body = [modtime.to_s]
69
+
70
+ header = {'Content-Type' => 'text/plain',
71
+ 'Content-Length' => body.join.size.to_s,
72
+ 'Last-Modified' => modtime.httpdate,
73
+ 'Cache-Control' => 'must-revalidate'}
74
+
75
+ [ code, header, body ]
76
+ end unless defined? ModifiedResponder
77
+
78
+ require 'rubygems'
79
+ require 'httpauth'
80
+ AuthorizationResponder = lambda do |env|
81
+ authtype = env['QUERY_STRING']
82
+ header = {}
83
+ if auth_string = env['HTTP_AUTHORIZATION']
84
+ if authtype == "basic" &&
85
+ ['admin', 'secret'] == HTTPAuth::Basic.unpack_authorization(auth_string)
86
+ code = 200
87
+ body = ["Authorized"]
88
+ elsif authtype == "digest" #&&
89
+ credentials = HTTPAuth::Digest::Credentials.from_header(auth_string) &&
90
+ credentials &&
91
+ credentials.validate(:password => 'secret', :method => 'GET')
92
+ code = 200
93
+ body = ["Authorized"]
94
+ else
95
+ code = 401
96
+ body = ["Not Authorized"]
97
+ end
98
+ else
99
+ code = 401
100
+ body = ["Not Authorized"]
101
+ if authtype == "basic"
102
+ header = {'WWW-Authenticate' => HTTPAuth::Basic.pack_challenge('Test Auth')}
103
+ elsif authtype == "digest"
104
+ chal = HTTPAuth::Digest::Credentials.new(:realm => 'Test Auth', :qop => 'auth')
105
+ header = {'WWW-Authenticate' => chal.to_header}
106
+ end
107
+ end
108
+
109
+ [ code, header.merge({'Content-Type' => 'text/plain', 'Content-Length' => body.join.size.to_s}), body ]
110
+ end unless defined? AuthorizationResponder
111
+
112
+ describe 'simple http server', :shared => true do
113
+ before(:all) do
114
+ #setup a thin http server we can connect to
115
+ require 'thin'
116
+ require 'rack'
117
+ require 'rack/lobster'
118
+
119
+ app = Rack::Builder.new do |env|
120
+ use Rack::ShowExceptions
121
+
122
+ map( '/get' ){ run SimpleGet }
123
+ map( '/post' ){ run SimplePost }
124
+ map( '/put' ){ run SimplePut }
125
+ map( '/delete' ){ run SimpleDel }
126
+
127
+ map( '/method' ){ run MethodResponder }
128
+ map( '/code' ){ run CodeResponder }
129
+ map( '/redirect' ){ run Redirector }
130
+ map( '/header' ){ run HeaderResponder }
131
+ map( '/modified' ){ run ModifiedResponder }
132
+ map( '/auth' ){ run AuthorizationResponder }
133
+ end
134
+
135
+ #spawn the server in a separate thread
136
+ @httpd = Thread.new do
137
+ Thin::Logging.silent = true
138
+ #Thin::Logging.debug = true
139
+ Thin::Server.start(app)
140
+ end
141
+ #give the server a chance to initialize
142
+ sleep 0.05
143
+ end
144
+
145
+ after(:all) do
146
+ # kill the server thread
147
+ @httpd.exit
148
+ end
149
+
150
+
151
+ end
@@ -0,0 +1,195 @@
1
+ require 'pathname'
2
+ require Pathname(__FILE__).dirname + 'spec_helper'
3
+ require 'rubygems'
4
+ require 'addressable/uri'
5
+
6
+ require 'resourceful/net_http_adapter'
7
+
8
+ describe 'http server' do
9
+ it_should_behave_like 'simple http server'
10
+
11
+ it 'should have a response code of 200 if the path is /get' do
12
+ Resourceful::NetHttpAdapter.make_request(:get, 'http://localhost:3000/get')[0].should == 200
13
+ end
14
+
15
+ it 'should reply with the posted document in the body if the path is /post' do
16
+ resp = Resourceful::NetHttpAdapter.make_request(:get, 'http://localhost:3000/post', 'Hello from POST!')
17
+ resp[2].should == 'Hello from POST!'
18
+ resp[0].should == 201
19
+ end
20
+
21
+ it 'should reply with the puted document in the body if the path is /put' do
22
+ resp = Resourceful::NetHttpAdapter.make_request(:put, 'http://localhost:3000/put', 'Hello from PUT!')
23
+ resp[2].should == 'Hello from PUT!'
24
+ resp[0].should == 200
25
+ end
26
+
27
+ it 'should reply with "KABOOM!" in the body if the path is /delete' do
28
+ resp = Resourceful::NetHttpAdapter.make_request(:delete, 'http://localhost:3000/delete')
29
+ resp[2].should == 'KABOOM!'
30
+ resp[0].should == 200
31
+ end
32
+
33
+ it 'should have a response code of whatever the path is' do
34
+ Resourceful::NetHttpAdapter.make_request(:get, 'http://localhost:3000/code/304')[0].should == 304
35
+ end
36
+
37
+ it 'should redirect to a given url' do
38
+ resp = Resourceful::NetHttpAdapter.make_request(:get, 'http://localhost:3000/redirect/301?http://localhost:3000/get')
39
+
40
+ resp[0].should == 301
41
+ resp[1]['Location'].should == ['http://localhost:3000/get']
42
+ end
43
+
44
+ it 'should respond with the request method in the body' do
45
+ resp = Resourceful::NetHttpAdapter.make_request(:delete, 'http://localhost:3000/method')
46
+
47
+ resp[0].should == 200
48
+ resp[2].should == "DELETE"
49
+ end
50
+
51
+ it 'should respond with the header set from the query string' do
52
+ uri = URI.escape('http://localhost:3000/header?{Foo: "bar"}')
53
+ resp = Resourceful::NetHttpAdapter.make_request(:get, uri)
54
+
55
+ resp[1].should have_key('Foo')
56
+ resp[1]['Foo'].should == ['bar']
57
+ end
58
+
59
+ it 'should parse escaped uris properly' do
60
+ uri = URI.escape("http://localhost:3000/header?{Expire: \"#{Time.now.httpdate}\"}")
61
+
62
+ resp = Resourceful::NetHttpAdapter.make_request(:get, uri)
63
+
64
+ resp[1].should have_key('Expire')
65
+ resp[1]['Expire'].first.should_not =~ /%/
66
+ end
67
+
68
+ describe '/modified' do
69
+ it 'should be 200 if no I-M-S header' do
70
+ uri = URI.escape("http://localhost:3000/modified?#{(Time.now + 3600).httpdate}")
71
+
72
+ resp = Resourceful::NetHttpAdapter.make_request(:get, uri)
73
+
74
+ resp[0].should == 200
75
+ end
76
+
77
+ it 'should be 304 if I-M-S header is set' do
78
+ now = Time.utc(2008,5,29,12,00)
79
+ uri = URI.escape("http://localhost:3000/modified?#{(now + 3600).httpdate}")
80
+
81
+ resp = Resourceful::NetHttpAdapter.make_request(:get, uri, nil, {'If-Modified-Since' => now.httpdate})
82
+
83
+ resp[0].should == 304
84
+ end
85
+
86
+ end
87
+
88
+ describe '/auth' do
89
+ before do
90
+ @uri = "http://localhost:3000/auth?basic"
91
+ end
92
+
93
+ it 'should return a 401 if no auth info is provided' do
94
+ resp = Resourceful::NetHttpAdapter.make_request(:get, @uri)
95
+ resp[0].should == 401
96
+ end
97
+
98
+ describe 'basic' do
99
+ before do
100
+ @uri = "http://localhost:3000/auth?basic"
101
+ end
102
+
103
+ it 'should return a 401 if no auth info is provided' do
104
+ resp = Resourceful::NetHttpAdapter.make_request(:get, @uri)
105
+ resp[0].should == 401
106
+ end
107
+
108
+ it 'should provide a WWW-Authenticate header when 401' do
109
+ resp = Resourceful::NetHttpAdapter.make_request(:get, @uri)
110
+ header = resp[1]
111
+ header.should have_key('WWW-Authenticate')
112
+ end
113
+
114
+ it 'should set the scheme to "Basic"' do
115
+ resp = Resourceful::NetHttpAdapter.make_request(:get, @uri)
116
+ auth = resp[1]['WWW-Authenticate'].first
117
+ auth.should =~ /^Basic/
118
+ end
119
+
120
+ it 'should set the realm to "Test Auth"' do
121
+ resp = Resourceful::NetHttpAdapter.make_request(:get, @uri)
122
+ auth = resp[1]['WWW-Authenticate'].first
123
+ auth.should =~ /realm="Test Auth"/
124
+ end
125
+
126
+ it 'should authorize on u/p:admin/secret' do
127
+ creds = HTTPAuth::Basic.pack_authorization('admin', 'secret')
128
+ header = {'Authorization' => creds}
129
+ resp = Resourceful::NetHttpAdapter.make_request(:get, @uri, nil, header)
130
+ resp[0].should == 200
131
+ end
132
+
133
+ it 'should authorize if u/p is incorrect' do
134
+ creds = HTTPAuth::Basic.pack_authorization('admin', 'not secret')
135
+ header = {'Authorization' => creds}
136
+ resp = Resourceful::NetHttpAdapter.make_request(:get, @uri, nil, header)
137
+ resp[0].should == 401
138
+ end
139
+
140
+ end
141
+
142
+ describe 'digest' do
143
+ before do
144
+ @uri = "http://localhost:3000/auth?digest"
145
+ end
146
+
147
+ it 'should return a 401 if no auth info is provided' do
148
+ resp = Resourceful::NetHttpAdapter.make_request(:get, @uri)
149
+ resp[0].should == 401
150
+ end
151
+
152
+ it 'should provide a WWW-Authenticate header when 401' do
153
+ resp = Resourceful::NetHttpAdapter.make_request(:get, @uri)
154
+ header = resp[1]
155
+ header.should have_key('WWW-Authenticate')
156
+ end
157
+
158
+ it 'should set the scheme to "Digest"' do
159
+ resp = Resourceful::NetHttpAdapter.make_request(:get, @uri)
160
+ auth = resp[1]['WWW-Authenticate'].first
161
+ auth.should =~ /^Digest/
162
+ end
163
+
164
+ it 'should set the realm to "Test Auth"' do
165
+ resp = Resourceful::NetHttpAdapter.make_request(:get, @uri)
166
+ auth = resp[1]['WWW-Authenticate'].first
167
+ auth.should =~ /realm="Test Auth"/
168
+ end
169
+
170
+ def challenge
171
+ resp = Resourceful::NetHttpAdapter.make_request(:get, @uri)
172
+ HTTPAuth::Digest::Challenge.from_header(resp[1]['WWW-Authenticate'].first)
173
+ end
174
+
175
+ it 'should authorize on u/p:admin/secret' do
176
+ creds = HTTPAuth::Digest::Credentials.from_challenge(challenge, :username => 'admin', :password => 'secret', :uri => @uri)
177
+ header = {'Authorization' => creds.to_header}
178
+ resp = Resourceful::NetHttpAdapter.make_request(:get, @uri, nil, header)
179
+ resp[0].should == 200
180
+ end
181
+
182
+ it 'should not authorize if u/p is incorrect' do
183
+ pending
184
+ creds = HTTPAuth::Digest::Credentials.from_challenge(challenge, :username => 'admin', :password => 'not secret', :uri => @uri)
185
+ header = {'Authorization' => creds.to_header}
186
+ resp = Resourceful::NetHttpAdapter.make_request(:get, @uri, nil, header)
187
+ resp[0].should == 401
188
+ end
189
+
190
+ end
191
+
192
+ end
193
+
194
+ end
195
+