josh-rack-cache 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,62 @@
1
+ require 'uri'
2
+ require 'rack/cache/metastore'
3
+ require 'rack/cache/entitystore'
4
+
5
+ module Rack::Cache
6
+
7
+ # Maintains a collection of MetaStore and EntityStore instances keyed by
8
+ # URI. A single instance of this class can be used across a single process
9
+ # to ensure that only a single instance of a backing store is created per
10
+ # unique storage URI.
11
+ class Storage
12
+ def initialize
13
+ @metastores = {}
14
+ @entitystores = {}
15
+ end
16
+
17
+ def resolve_metastore_uri(uri)
18
+ @metastores[uri.to_s] ||= create_store(MetaStore, uri)
19
+ end
20
+
21
+ def resolve_entitystore_uri(uri)
22
+ @entitystores[uri.to_s] ||= create_store(EntityStore, uri)
23
+ end
24
+
25
+ def clear
26
+ @metastores.clear
27
+ @entitystores.clear
28
+ nil
29
+ end
30
+
31
+ private
32
+ def create_store(type, uri)
33
+ if uri.respond_to?(:scheme) || uri.respond_to?(:to_str)
34
+ uri = URI.parse(uri) unless uri.respond_to?(:scheme)
35
+ if type.const_defined?(uri.scheme.upcase)
36
+ klass = type.const_get(uri.scheme.upcase)
37
+ klass.resolve(uri)
38
+ else
39
+ fail "Unknown storage provider: #{uri.to_s}"
40
+ end
41
+ else
42
+ # hack in support for passing a MemCache or Memcached object
43
+ # as the storage URI.
44
+ case
45
+ when defined?(::MemCache) && uri.kind_of?(::MemCache)
46
+ type.const_get(:MemCache).resolve(uri)
47
+ when defined?(::Memcached) && uri.respond_to?(:stats)
48
+ type.const_get(:MemCached).resolve(uri)
49
+ else
50
+ fail "Unknown storage provider: #{uri.to_s}"
51
+ end
52
+ end
53
+ end
54
+
55
+ public
56
+ @@singleton_instance = new
57
+ def self.instance
58
+ @@singleton_instance
59
+ end
60
+ end
61
+
62
+ end
@@ -0,0 +1,70 @@
1
+ Gem::Specification.new do |s|
2
+ s.specification_version = 2 if s.respond_to? :specification_version=
3
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
4
+
5
+ s.name = 'rack-cache'
6
+ s.version = '0.5.1'
7
+ s.date = '2009-06-06'
8
+
9
+ s.description = "HTTP Caching for Rack"
10
+ s.summary = "HTTP Caching for Rack"
11
+
12
+ s.authors = ["Ryan Tomayko"]
13
+ s.email = "r@tomayko.com"
14
+
15
+ # = MANIFEST =
16
+ s.files = %w[
17
+ CHANGES
18
+ COPYING
19
+ README
20
+ Rakefile
21
+ TODO
22
+ doc/configuration.markdown
23
+ doc/faq.markdown
24
+ doc/index.markdown
25
+ doc/layout.html.erb
26
+ doc/license.markdown
27
+ doc/rack-cache.css
28
+ doc/server.ru
29
+ doc/storage.markdown
30
+ example/sinatra/app.rb
31
+ example/sinatra/views/index.erb
32
+ lib/rack/cache.rb
33
+ lib/rack/cache/appengine.rb
34
+ lib/rack/cache/cachecontrol.rb
35
+ lib/rack/cache/context.rb
36
+ lib/rack/cache/entitystore.rb
37
+ lib/rack/cache/key.rb
38
+ lib/rack/cache/metastore.rb
39
+ lib/rack/cache/options.rb
40
+ lib/rack/cache/request.rb
41
+ lib/rack/cache/response.rb
42
+ lib/rack/cache/storage.rb
43
+ rack-cache.gemspec
44
+ test/cache_test.rb
45
+ test/cachecontrol_test.rb
46
+ test/context_test.rb
47
+ test/entitystore_test.rb
48
+ test/key_test.rb
49
+ test/metastore_test.rb
50
+ test/options_test.rb
51
+ test/pony.jpg
52
+ test/request_test.rb
53
+ test/response_test.rb
54
+ test/spec_setup.rb
55
+ test/storage_test.rb
56
+ ]
57
+ # = MANIFEST =
58
+
59
+ s.test_files = s.files.select {|path| path =~ /^test\/.*_test.rb/}
60
+
61
+ s.extra_rdoc_files = %w[README COPYING TODO CHANGES]
62
+ s.add_dependency 'rack', '>= 0.4'
63
+
64
+ s.has_rdoc = true
65
+ s.homepage = "http://tomayko.com/src/rack-cache/"
66
+ s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "Rack::Cache", "--main", "Rack::Cache"]
67
+ s.require_paths = %w[lib]
68
+ s.rubyforge_project = 'wink'
69
+ s.rubygems_version = '1.1.1'
70
+ end
@@ -0,0 +1,38 @@
1
+ require "#{File.dirname(__FILE__)}/spec_setup"
2
+
3
+ def dumb_app(env)
4
+ body = block_given? ? [yield] : ['Hi']
5
+ [ 200, {'Content-Type' => 'text/plain'}, body ]
6
+ end
7
+
8
+ describe 'Rack::Cache::new' do
9
+ before { @app = method(:dumb_app) }
10
+
11
+ it 'takes a backend and returns a middleware component' do
12
+ Rack::Cache.new(@app).
13
+ should.respond_to :call
14
+ end
15
+
16
+ it 'takes an options Hash' do
17
+ lambda { Rack::Cache.new(@app, {}) }.
18
+ should.not.raise(ArgumentError)
19
+ end
20
+
21
+ it 'sets options provided in the options Hash' do
22
+ object = Rack::Cache.new(@app, :foo => 'bar', 'foo.bar' => 'bling')
23
+ object.options['foo.bar'].should.equal 'bling'
24
+ object.options['rack-cache.foo'].should.equal 'bar'
25
+ end
26
+
27
+ it 'takes a block; executes it during initialization' do
28
+ state, object = 'not invoked', nil
29
+ instance =
30
+ Rack::Cache.new @app do |cache|
31
+ object = cache
32
+ state = 'invoked'
33
+ cache.should.respond_to :set
34
+ end
35
+ state.should.equal 'invoked'
36
+ object.should.be instance
37
+ end
38
+ end
@@ -0,0 +1,139 @@
1
+ require "#{File.dirname(__FILE__)}/spec_setup"
2
+ require 'rack/cache/cachecontrol'
3
+
4
+ describe 'Rack::Cache::CacheControl' do
5
+ it 'takes no args and initializes with an empty set of values' do
6
+ cache_control = Rack::Cache::CacheControl.new
7
+ cache_control.should.be.empty
8
+ cache_control.to_s.should.equal ''
9
+ end
10
+
11
+ it 'takes a String and parses it into a Hash when created' do
12
+ cache_control = Rack::Cache::CacheControl.new('max-age=600, foo')
13
+ cache_control['max-age'].should.equal '600'
14
+ cache_control['foo'].should.be true
15
+ end
16
+
17
+ it 'takes a String with a single name=value pair' do
18
+ cache_control = Rack::Cache::CacheControl.new('max-age=600')
19
+ cache_control['max-age'].should.equal '600'
20
+ end
21
+
22
+ it 'takes a String with multiple name=value pairs' do
23
+ cache_control = Rack::Cache::CacheControl.new('max-age=600, max-stale=300, min-fresh=570')
24
+ cache_control['max-age'].should.equal '600'
25
+ cache_control['max-stale'].should.equal '300'
26
+ cache_control['min-fresh'].should.equal '570'
27
+ end
28
+
29
+ it 'takes a String with a single flag value' do
30
+ cache_control = Rack::Cache::CacheControl.new('no-cache')
31
+ cache_control.should.include 'no-cache'
32
+ cache_control['no-cache'].should.be true
33
+ end
34
+
35
+ it 'takes a String with a bunch of all kinds of stuff' do
36
+ cache_control =
37
+ Rack::Cache::CacheControl.new('max-age=600,must-revalidate,min-fresh=3000,foo=bar,baz')
38
+ cache_control['max-age'].should.equal '600'
39
+ cache_control['must-revalidate'].should.be true
40
+ cache_control['min-fresh'].should.equal '3000'
41
+ cache_control['foo'].should.equal 'bar'
42
+ cache_control['baz'].should.be true
43
+ end
44
+
45
+ it 'strips leading and trailing spaces from header value' do
46
+ cache_control = Rack::Cache::CacheControl.new(' public, max-age = 600 ')
47
+ cache_control.should.include 'public'
48
+ cache_control.should.include 'max-age'
49
+ cache_control['max-age'].should.equal '600'
50
+ end
51
+
52
+ it 'removes all directives with #clear' do
53
+ cache_control = Rack::Cache::CacheControl.new('max-age=600, must-revalidate')
54
+ cache_control.clear
55
+ cache_control.should.be.empty
56
+ end
57
+
58
+ it 'converts self into header String with #to_s' do
59
+ cache_control = Rack::Cache::CacheControl.new
60
+ cache_control['public'] = true
61
+ cache_control['max-age'] = '600'
62
+ cache_control.to_s.split(', ').sort.should.equal ['max-age=600', 'public']
63
+ end
64
+
65
+ it 'sorts alphabetically with boolean directives before value directives' do
66
+ cache_control = Rack::Cache::CacheControl.new('foo=bar, z, x, y, bling=baz, zoom=zib, b, a')
67
+ cache_control.to_s.should.equal 'a, b, x, y, z, bling=baz, foo=bar, zoom=zib'
68
+ end
69
+
70
+ it 'responds to #max_age with an integer when max-age directive present' do
71
+ cache_control = Rack::Cache::CacheControl.new('public, max-age=600')
72
+ cache_control.max_age.should.equal 600
73
+ end
74
+
75
+ it 'responds to #max_age with nil when no max-age directive present' do
76
+ cache_control = Rack::Cache::CacheControl.new('public')
77
+ cache_control.max_age.should.be nil
78
+ end
79
+
80
+ it 'responds to #shared_max_age with an integer when s-maxage directive present' do
81
+ cache_control = Rack::Cache::CacheControl.new('public, s-maxage=600')
82
+ cache_control.shared_max_age.should.equal 600
83
+ end
84
+
85
+ it 'responds to #shared_max_age with nil when no s-maxage directive present' do
86
+ cache_control = Rack::Cache::CacheControl.new('public')
87
+ cache_control.shared_max_age.should.be nil
88
+ end
89
+
90
+ it 'responds to #public? truthfully when public directive present' do
91
+ cache_control = Rack::Cache::CacheControl.new('public')
92
+ cache_control.should.be.public
93
+ end
94
+
95
+ it 'responds to #public? non-truthfully when no public directive present' do
96
+ cache_control = Rack::Cache::CacheControl.new('private')
97
+ cache_control.should.not.be.public
98
+ end
99
+
100
+ it 'responds to #private? truthfully when private directive present' do
101
+ cache_control = Rack::Cache::CacheControl.new('private')
102
+ cache_control.should.be.private
103
+ end
104
+
105
+ it 'responds to #private? non-truthfully when no private directive present' do
106
+ cache_control = Rack::Cache::CacheControl.new('public')
107
+ cache_control.should.not.be.private
108
+ end
109
+
110
+ it 'responds to #no_cache? truthfully when no-cache directive present' do
111
+ cache_control = Rack::Cache::CacheControl.new('no-cache')
112
+ cache_control.should.be.no_cache
113
+ end
114
+
115
+ it 'responds to #no_cache? non-truthfully when no no-cache directive present' do
116
+ cache_control = Rack::Cache::CacheControl.new('max-age=600')
117
+ cache_control.should.not.be.no_cache
118
+ end
119
+
120
+ it 'responds to #must_revalidate? truthfully when must-revalidate directive present' do
121
+ cache_control = Rack::Cache::CacheControl.new('must-revalidate')
122
+ cache_control.should.be.must_revalidate
123
+ end
124
+
125
+ it 'responds to #must_revalidate? non-truthfully when no must-revalidate directive present' do
126
+ cache_control = Rack::Cache::CacheControl.new('max-age=600')
127
+ cache_control.should.not.be.no_cache
128
+ end
129
+
130
+ it 'responds to #proxy_revalidate? truthfully when proxy-revalidate directive present' do
131
+ cache_control = Rack::Cache::CacheControl.new('proxy-revalidate')
132
+ cache_control.should.be.proxy_revalidate
133
+ end
134
+
135
+ it 'responds to #proxy_revalidate? non-truthfully when no proxy-revalidate directive present' do
136
+ cache_control = Rack::Cache::CacheControl.new('max-age=600')
137
+ cache_control.should.not.be.no_cache
138
+ end
139
+ end
@@ -0,0 +1,774 @@
1
+ require "#{File.dirname(__FILE__)}/spec_setup"
2
+ require 'rack/cache/context'
3
+
4
+ describe 'Rack::Cache::Context' do
5
+ before(:each) { setup_cache_context }
6
+ after(:each) { teardown_cache_context }
7
+
8
+ it 'passes on non-GET/HEAD requests' do
9
+ respond_with 200
10
+ post '/'
11
+
12
+ app.should.be.called
13
+ response.should.be.ok
14
+ cache.trace.should.include :pass
15
+ response.headers.should.not.include 'Age'
16
+ end
17
+
18
+ %w[post put delete].each do |request_method|
19
+ it "invalidates on #{request_method} requests" do
20
+ respond_with 200
21
+ request request_method, '/'
22
+
23
+ app.should.be.called
24
+ response.should.be.ok
25
+ cache.trace.should.include :invalidate
26
+ cache.trace.should.include :pass
27
+ end
28
+ end
29
+
30
+ it 'does not cache with Authorization request header and non public response' do
31
+ respond_with 200, 'ETag' => '"FOO"'
32
+ get '/', 'HTTP_AUTHORIZATION' => 'basic foobarbaz'
33
+
34
+ app.should.be.called
35
+ response.should.be.ok
36
+ response.headers['Cache-Control'].should.equal 'private'
37
+ cache.trace.should.include :miss
38
+ cache.trace.should.not.include :store
39
+ response.headers.should.not.include 'Age'
40
+ end
41
+
42
+ it 'does cache with Authorization request header and public response' do
43
+ respond_with 200, 'Cache-Control' => 'public', 'ETag' => '"FOO"'
44
+ get '/', 'HTTP_AUTHORIZATION' => 'basic foobarbaz'
45
+
46
+ app.should.be.called
47
+ response.should.be.ok
48
+ cache.trace.should.include :miss
49
+ cache.trace.should.include :store
50
+ response.headers.should.include 'Age'
51
+ response.headers['Cache-Control'].should.equal 'public'
52
+ end
53
+
54
+ it 'does not cache with Cookie header and non public response' do
55
+ respond_with 200, 'ETag' => '"FOO"'
56
+ get '/', 'HTTP_COOKIE' => 'foo=bar'
57
+
58
+ app.should.be.called
59
+ response.should.be.ok
60
+ response.headers['Cache-Control'].should.equal 'private'
61
+ cache.trace.should.include :miss
62
+ cache.trace.should.not.include :store
63
+ response.headers.should.not.include 'Age'
64
+ end
65
+
66
+ it 'does not cache requests with a Cookie header' do
67
+ respond_with 200
68
+ get '/', 'HTTP_COOKIE' => 'foo=bar'
69
+
70
+ response.should.be.ok
71
+ app.should.be.called
72
+ cache.trace.should.include :miss
73
+ cache.trace.should.not.include :store
74
+ response.headers.should.not.include 'Age'
75
+ response.headers['Cache-Control'].should.equal 'private'
76
+ end
77
+
78
+ it 'responds with 304 when If-Modified-Since matches Last-Modified' do
79
+ timestamp = Time.now.httpdate
80
+ respond_with do |req,res|
81
+ res.status = 200
82
+ res['Last-Modified'] = timestamp
83
+ res['Content-Type'] = 'text/plain'
84
+ res.body = ['Hello World']
85
+ end
86
+
87
+ get '/',
88
+ 'HTTP_IF_MODIFIED_SINCE' => timestamp
89
+ app.should.be.called
90
+ response.status.should.equal 304
91
+ response.headers.should.not.include 'Content-Length'
92
+ response.headers.should.not.include 'Content-Type'
93
+ response.body.should.empty
94
+ cache.trace.should.include :miss
95
+ cache.trace.should.include :store
96
+ end
97
+
98
+ it 'responds with 304 when If-None-Match matches ETag' do
99
+ respond_with do |req,res|
100
+ res.status = 200
101
+ res['ETag'] = '12345'
102
+ res['Content-Type'] = 'text/plain'
103
+ res.body = ['Hello World']
104
+ end
105
+
106
+ get '/',
107
+ 'HTTP_IF_NONE_MATCH' => '12345'
108
+ app.should.be.called
109
+ response.status.should.equal 304
110
+ response.headers.should.not.include 'Content-Length'
111
+ response.headers.should.not.include 'Content-Type'
112
+ response.headers.should.include 'ETag'
113
+ response.body.should.empty
114
+ cache.trace.should.include :miss
115
+ cache.trace.should.include :store
116
+ end
117
+
118
+ it 'stores responses when no-cache request directive present' do
119
+ respond_with 200, 'Expires' => (Time.now + 5).httpdate
120
+
121
+ get '/', 'HTTP_CACHE_CONTROL' => 'no-cache'
122
+ response.should.be.ok
123
+ cache.trace.should.include :store
124
+ response.headers.should.include 'Age'
125
+ end
126
+
127
+ it 'reloads responses when cache hits but no-cache request directive present ' +
128
+ 'when allow_reload is set true' do
129
+ count = 0
130
+ respond_with 200, 'Cache-Control' => 'max-age=10000' do |req,res|
131
+ count+= 1
132
+ res.body = (count == 1) ? ['Hello World'] : ['Goodbye World']
133
+ end
134
+
135
+ get '/'
136
+ response.should.be.ok
137
+ response.body.should.equal 'Hello World'
138
+ cache.trace.should.include :store
139
+
140
+ get '/'
141
+ response.should.be.ok
142
+ response.body.should.equal 'Hello World'
143
+ cache.trace.should.include :fresh
144
+
145
+ get '/',
146
+ 'rack-cache.allow_reload' => true,
147
+ 'HTTP_CACHE_CONTROL' => 'no-cache'
148
+ response.should.be.ok
149
+ response.body.should.equal 'Goodbye World'
150
+ cache.trace.should.include :reload
151
+ cache.trace.should.include :store
152
+ end
153
+
154
+ it 'does not reload responses when allow_reload is set false (default)' do
155
+ count = 0
156
+ respond_with 200, 'Cache-Control' => 'max-age=10000' do |req,res|
157
+ count+= 1
158
+ res.body = (count == 1) ? ['Hello World'] : ['Goodbye World']
159
+ end
160
+
161
+ get '/'
162
+ response.should.be.ok
163
+ response.body.should.equal 'Hello World'
164
+ cache.trace.should.include :store
165
+
166
+ get '/'
167
+ response.should.be.ok
168
+ response.body.should.equal 'Hello World'
169
+ cache.trace.should.include :fresh
170
+
171
+ get '/',
172
+ 'rack-cache.allow_reload' => false,
173
+ 'HTTP_CACHE_CONTROL' => 'no-cache'
174
+ response.should.be.ok
175
+ response.body.should.equal 'Hello World'
176
+ cache.trace.should.not.include :reload
177
+
178
+ # test again without explicitly setting the allow_reload option to false
179
+ get '/',
180
+ 'HTTP_CACHE_CONTROL' => 'no-cache'
181
+ response.should.be.ok
182
+ response.body.should.equal 'Hello World'
183
+ cache.trace.should.not.include :reload
184
+ end
185
+
186
+ it 'revalidates fresh cache entry when max-age request directive is exceeded ' +
187
+ 'when allow_revalidate option is set true' do
188
+ count = 0
189
+ respond_with do |req,res|
190
+ count+= 1
191
+ res['Cache-Control'] = 'max-age=10000'
192
+ res['ETag'] = count.to_s
193
+ res.body = (count == 1) ? ['Hello World'] : ['Goodbye World']
194
+ end
195
+
196
+ get '/'
197
+ response.should.be.ok
198
+ response.body.should.equal 'Hello World'
199
+ cache.trace.should.include :store
200
+
201
+ get '/'
202
+ response.should.be.ok
203
+ response.body.should.equal 'Hello World'
204
+ cache.trace.should.include :fresh
205
+
206
+ get '/',
207
+ 'rack-cache.allow_revalidate' => true,
208
+ 'HTTP_CACHE_CONTROL' => 'max-age=0'
209
+ response.should.be.ok
210
+ response.body.should.equal 'Goodbye World'
211
+ cache.trace.should.include :stale
212
+ cache.trace.should.include :invalid
213
+ cache.trace.should.include :store
214
+ end
215
+
216
+ it 'does not revalidate fresh cache entry when enable_revalidate option is set false (default)' do
217
+ count = 0
218
+ respond_with do |req,res|
219
+ count+= 1
220
+ res['Cache-Control'] = 'max-age=10000'
221
+ res['ETag'] = count.to_s
222
+ res.body = (count == 1) ? ['Hello World'] : ['Goodbye World']
223
+ end
224
+
225
+ get '/'
226
+ response.should.be.ok
227
+ response.body.should.equal 'Hello World'
228
+ cache.trace.should.include :store
229
+
230
+ get '/'
231
+ response.should.be.ok
232
+ response.body.should.equal 'Hello World'
233
+ cache.trace.should.include :fresh
234
+
235
+ get '/',
236
+ 'rack-cache.allow_revalidate' => false,
237
+ 'HTTP_CACHE_CONTROL' => 'max-age=0'
238
+ response.should.be.ok
239
+ response.body.should.equal 'Hello World'
240
+ cache.trace.should.not.include :stale
241
+ cache.trace.should.not.include :invalid
242
+ cache.trace.should.include :fresh
243
+
244
+ # test again without explicitly setting the allow_revalidate option to false
245
+ get '/',
246
+ 'HTTP_CACHE_CONTROL' => 'max-age=0'
247
+ response.should.be.ok
248
+ response.body.should.equal 'Hello World'
249
+ cache.trace.should.not.include :stale
250
+ cache.trace.should.not.include :invalid
251
+ cache.trace.should.include :fresh
252
+ end
253
+ it 'fetches response from backend when cache misses' do
254
+ respond_with 200, 'Expires' => (Time.now + 5).httpdate
255
+ get '/'
256
+
257
+ response.should.be.ok
258
+ cache.trace.should.include :miss
259
+ response.headers.should.include 'Age'
260
+ end
261
+
262
+ [(201..202),(204..206),(303..305),(400..403),(405..409),(411..417),(500..505)].each do |range|
263
+ range.each do |response_code|
264
+ it "does not cache #{response_code} responses" do
265
+ respond_with response_code, 'Expires' => (Time.now + 5).httpdate
266
+ get '/'
267
+
268
+ cache.trace.should.not.include :store
269
+ response.status.should.equal response_code
270
+ response.headers.should.not.include 'Age'
271
+ end
272
+ end
273
+ end
274
+
275
+ it "does not cache responses with explicit no-store directive" do
276
+ respond_with 200,
277
+ 'Expires' => (Time.now + 5).httpdate,
278
+ 'Cache-Control' => 'no-store'
279
+ get '/'
280
+
281
+ response.should.be.ok
282
+ cache.trace.should.not.include :store
283
+ response.headers.should.not.include 'Age'
284
+ end
285
+
286
+ it 'does not cache responses without freshness information or a validator' do
287
+ respond_with 200
288
+ get '/'
289
+
290
+ response.should.be.ok
291
+ cache.trace.should.not.include :store
292
+ end
293
+
294
+ it "caches responses with explicit no-cache directive" do
295
+ respond_with 200,
296
+ 'Expires' => (Time.now + 5).httpdate,
297
+ 'Cache-Control' => 'no-cache'
298
+ get '/'
299
+
300
+ response.should.be.ok
301
+ cache.trace.should.include :store
302
+ response.headers.should.include 'Age'
303
+ end
304
+
305
+ it 'caches responses with an Expiration header' do
306
+ respond_with 200, 'Expires' => (Time.now + 5).httpdate
307
+ get '/'
308
+
309
+ response.should.be.ok
310
+ response.body.should.equal 'Hello World'
311
+ response.headers.should.include 'Date'
312
+ response['Age'].should.not.be.nil
313
+ response['X-Content-Digest'].should.not.be.nil
314
+ cache.trace.should.include :miss
315
+ cache.trace.should.include :store
316
+ cache.metastore.to_hash.keys.length.should.equal 1
317
+ end
318
+
319
+ it 'caches responses with a max-age directive' do
320
+ respond_with 200, 'Cache-Control' => 'max-age=5'
321
+ get '/'
322
+
323
+ response.should.be.ok
324
+ response.body.should.equal 'Hello World'
325
+ response.headers.should.include 'Date'
326
+ response['Age'].should.not.be.nil
327
+ response['X-Content-Digest'].should.not.be.nil
328
+ cache.trace.should.include :miss
329
+ cache.trace.should.include :store
330
+ cache.metastore.to_hash.keys.length.should.equal 1
331
+ end
332
+
333
+ it 'caches responses with a s-maxage directive' do
334
+ respond_with 200, 'Cache-Control' => 's-maxage=5'
335
+ get '/'
336
+
337
+ response.should.be.ok
338
+ response.body.should.equal 'Hello World'
339
+ response.headers.should.include 'Date'
340
+ response['Age'].should.not.be.nil
341
+ response['X-Content-Digest'].should.not.be.nil
342
+ cache.trace.should.include :miss
343
+ cache.trace.should.include :store
344
+ cache.metastore.to_hash.keys.length.should.equal 1
345
+ end
346
+
347
+ it 'caches responses with a Last-Modified validator but no freshness information' do
348
+ respond_with 200, 'Last-Modified' => Time.now.httpdate
349
+ get '/'
350
+
351
+ response.should.be.ok
352
+ response.body.should.equal 'Hello World'
353
+ cache.trace.should.include :miss
354
+ cache.trace.should.include :store
355
+ end
356
+
357
+ it 'caches responses with an ETag validator but no freshness information' do
358
+ respond_with 200, 'ETag' => '"123456"'
359
+ get '/'
360
+
361
+ response.should.be.ok
362
+ response.body.should.equal 'Hello World'
363
+ cache.trace.should.include :miss
364
+ cache.trace.should.include :store
365
+ end
366
+
367
+ it 'hits cached response with Expires header' do
368
+ respond_with 200,
369
+ 'Date' => (Time.now - 5).httpdate,
370
+ 'Expires' => (Time.now + 5).httpdate
371
+
372
+ get '/'
373
+ app.should.be.called
374
+ response.should.be.ok
375
+ response.headers.should.include 'Date'
376
+ cache.trace.should.include :miss
377
+ cache.trace.should.include :store
378
+ response.body.should.equal 'Hello World'
379
+
380
+ get '/'
381
+ response.should.be.ok
382
+ app.should.not.be.called
383
+ response['Date'].should.equal responses.first['Date']
384
+ response['Age'].to_i.should.satisfy { |age| age > 0 }
385
+ response['X-Content-Digest'].should.not.be.nil
386
+ cache.trace.should.include :fresh
387
+ cache.trace.should.not.include :store
388
+ response.body.should.equal 'Hello World'
389
+ end
390
+
391
+ it 'hits cached response with max-age directive' do
392
+ respond_with 200,
393
+ 'Date' => (Time.now - 5).httpdate,
394
+ 'Cache-Control' => 'max-age=10'
395
+
396
+ get '/'
397
+ app.should.be.called
398
+ response.should.be.ok
399
+ response.headers.should.include 'Date'
400
+ cache.trace.should.include :miss
401
+ cache.trace.should.include :store
402
+ response.body.should.equal 'Hello World'
403
+
404
+ get '/'
405
+ response.should.be.ok
406
+ app.should.not.be.called
407
+ response['Date'].should.equal responses.first['Date']
408
+ response['Age'].to_i.should.satisfy { |age| age > 0 }
409
+ response['X-Content-Digest'].should.not.be.nil
410
+ cache.trace.should.include :fresh
411
+ cache.trace.should.not.include :store
412
+ response.body.should.equal 'Hello World'
413
+ end
414
+
415
+ it 'hits cached response with s-maxage directive' do
416
+ respond_with 200,
417
+ 'Date' => (Time.now - 5).httpdate,
418
+ 'Cache-Control' => 's-maxage=10, max-age=0'
419
+
420
+ get '/'
421
+ app.should.be.called
422
+ response.should.be.ok
423
+ response.headers.should.include 'Date'
424
+ cache.trace.should.include :miss
425
+ cache.trace.should.include :store
426
+ response.body.should.equal 'Hello World'
427
+
428
+ get '/'
429
+ response.should.be.ok
430
+ app.should.not.be.called
431
+ response['Date'].should.equal responses.first['Date']
432
+ response['Age'].to_i.should.satisfy { |age| age > 0 }
433
+ response['X-Content-Digest'].should.not.be.nil
434
+ cache.trace.should.include :fresh
435
+ cache.trace.should.not.include :store
436
+ response.body.should.equal 'Hello World'
437
+ end
438
+
439
+ it 'assigns default_ttl when response has no freshness information' do
440
+ respond_with 200
441
+
442
+ get '/', 'rack-cache.default_ttl' => 10
443
+ app.should.be.called
444
+ response.should.be.ok
445
+ cache.trace.should.include :miss
446
+ cache.trace.should.include :store
447
+ response.body.should.equal 'Hello World'
448
+ response['Cache-Control'].should.include 's-maxage=10'
449
+
450
+ get '/', 'rack-cache.default_ttl' => 10
451
+ response.should.be.ok
452
+ app.should.not.be.called
453
+ cache.trace.should.include :fresh
454
+ cache.trace.should.not.include :store
455
+ response.body.should.equal 'Hello World'
456
+ end
457
+
458
+ it 'does not assign default_ttl when response has must-revalidate directive' do
459
+ respond_with 200,
460
+ 'Cache-Control' => 'must-revalidate'
461
+
462
+ get '/', 'rack-cache.default_ttl' => 10
463
+ app.should.be.called
464
+ response.should.be.ok
465
+ cache.trace.should.include :miss
466
+ cache.trace.should.not.include :store
467
+ response['Cache-Control'].should.not.include 's-maxage'
468
+ response.body.should.equal 'Hello World'
469
+ end
470
+
471
+ it 'fetches full response when cache stale and no validators present' do
472
+ respond_with 200, 'Expires' => (Time.now + 5).httpdate
473
+
474
+ # build initial request
475
+ get '/'
476
+ app.should.be.called
477
+ response.should.be.ok
478
+ response.headers.should.include 'Date'
479
+ response.headers.should.include 'X-Content-Digest'
480
+ response.headers.should.include 'Age'
481
+ cache.trace.should.include :miss
482
+ cache.trace.should.include :store
483
+ response.body.should.equal 'Hello World'
484
+
485
+ # go in and play around with the cached metadata directly ...
486
+ cache.metastore.to_hash.values.length.should.equal 1
487
+ cache.metastore.to_hash.values.first.first[1]['Expires'] = Time.now.httpdate
488
+
489
+ # build subsequent request; should be found but miss due to freshness
490
+ get '/'
491
+ app.should.be.called
492
+ response.should.be.ok
493
+ response['Age'].to_i.should.equal 0
494
+ response.headers.should.include 'X-Content-Digest'
495
+ cache.trace.should.include :stale
496
+ cache.trace.should.not.include :fresh
497
+ cache.trace.should.not.include :miss
498
+ cache.trace.should.include :store
499
+ response.body.should.equal 'Hello World'
500
+ end
501
+
502
+ it 'validates cached responses with Last-Modified and no freshness information' do
503
+ timestamp = Time.now.httpdate
504
+ respond_with do |req,res|
505
+ res['Last-Modified'] = timestamp
506
+ if req.env['HTTP_IF_MODIFIED_SINCE'] == timestamp
507
+ res.status = 304
508
+ res.body = []
509
+ end
510
+ end
511
+
512
+ # build initial request
513
+ get '/'
514
+ app.should.be.called
515
+ response.should.be.ok
516
+ response.headers.should.include 'Last-Modified'
517
+ response.headers.should.include 'X-Content-Digest'
518
+ response.body.should.equal 'Hello World'
519
+ cache.trace.should.include :miss
520
+ cache.trace.should.include :store
521
+ cache.trace.should.not.include :stale
522
+
523
+ # build subsequent request; should be found but miss due to freshness
524
+ get '/'
525
+ app.should.be.called
526
+ response.should.be.ok
527
+ response.headers.should.include 'Last-Modified'
528
+ response.headers.should.include 'X-Content-Digest'
529
+ response['Age'].to_i.should.equal 0
530
+ response.body.should.equal 'Hello World'
531
+ cache.trace.should.include :stale
532
+ cache.trace.should.include :valid
533
+ cache.trace.should.include :store
534
+ cache.trace.should.not.include :miss
535
+ end
536
+
537
+ it 'validates cached responses with ETag and no freshness information' do
538
+ timestamp = Time.now.httpdate
539
+ respond_with do |req,res|
540
+ res['ETAG'] = '"12345"'
541
+ if req.env['HTTP_IF_NONE_MATCH'] == res['Etag']
542
+ res.status = 304
543
+ res.body = []
544
+ end
545
+ end
546
+
547
+ # build initial request
548
+ get '/'
549
+ app.should.be.called
550
+ response.should.be.ok
551
+ response.headers.should.include 'ETag'
552
+ response.headers.should.include 'X-Content-Digest'
553
+ response.body.should.equal 'Hello World'
554
+ cache.trace.should.include :miss
555
+ cache.trace.should.include :store
556
+
557
+ # build subsequent request; should be found but miss due to freshness
558
+ get '/'
559
+ app.should.be.called
560
+ response.should.be.ok
561
+ response.headers.should.include 'ETag'
562
+ response.headers.should.include 'X-Content-Digest'
563
+ response['Age'].to_i.should.equal 0
564
+ response.body.should.equal 'Hello World'
565
+ cache.trace.should.include :stale
566
+ cache.trace.should.include :valid
567
+ cache.trace.should.include :store
568
+ cache.trace.should.not.include :miss
569
+ end
570
+
571
+ it 'replaces cached responses when validation results in non-304 response' do
572
+ timestamp = Time.now.httpdate
573
+ count = 0
574
+ respond_with do |req,res|
575
+ res['Last-Modified'] = timestamp
576
+ case (count+=1)
577
+ when 1 ; res.body = ['first response']
578
+ when 2 ; res.body = ['second response']
579
+ when 3
580
+ res.body = []
581
+ res.status = 304
582
+ end
583
+ end
584
+
585
+ # first request should fetch from backend and store in cache
586
+ get '/'
587
+ response.status.should.equal 200
588
+ response.body.should.equal 'first response'
589
+
590
+ # second request is validated, is invalid, and replaces cached entry
591
+ get '/'
592
+ response.status.should.equal 200
593
+ response.body.should.equal 'second response'
594
+
595
+ # third respone is validated, valid, and returns cached entry
596
+ get '/'
597
+ response.status.should.equal 200
598
+ response.body.should.equal 'second response'
599
+
600
+ count.should.equal 3
601
+ end
602
+
603
+ it 'passes HEAD requests through directly on pass' do
604
+ respond_with do |req,res|
605
+ res.status = 200
606
+ res.body = []
607
+ req.request_method.should.equal 'HEAD'
608
+ end
609
+
610
+ head '/', 'HTTP_EXPECT' => 'something ...'
611
+ app.should.be.called
612
+ response.body.should.equal ''
613
+ end
614
+
615
+ it 'uses cache to respond to HEAD requests when fresh' do
616
+ respond_with do |req,res|
617
+ res['Cache-Control'] = 'max-age=10'
618
+ res.body = ['Hello World']
619
+ req.request_method.should.not.equal 'HEAD'
620
+ end
621
+
622
+ get '/'
623
+ app.should.be.called
624
+ response.status.should.equal 200
625
+ response.body.should.equal 'Hello World'
626
+
627
+ head '/'
628
+ app.should.not.be.called
629
+ response.status.should.equal 200
630
+ response.body.should.equal ''
631
+ response['Content-Length'].should.equal 'Hello World'.length.to_s
632
+ end
633
+
634
+ it 'invalidates cached responses on POST' do
635
+ respond_with do |req,res|
636
+ if req.request_method == 'GET'
637
+ res.status = 200
638
+ res['Cache-Control'] = 'public, max-age=500'
639
+ res.body = ['Hello World']
640
+ elsif req.request_method == 'POST'
641
+ res.status = 303
642
+ res['Location'] = '/'
643
+ res.headers.delete('Cache-Control')
644
+ res.body = []
645
+ end
646
+ end
647
+
648
+ # build initial request to enter into the cache
649
+ get '/'
650
+ app.should.be.called
651
+ response.should.be.ok
652
+ response.body.should.equal 'Hello World'
653
+ cache.trace.should.include :miss
654
+ cache.trace.should.include :store
655
+
656
+ # make sure it is valid
657
+ get '/'
658
+ app.should.not.called
659
+ response.should.be.ok
660
+ response.body.should.equal 'Hello World'
661
+ cache.trace.should.include :fresh
662
+
663
+ # now POST to same URL
664
+ post '/'
665
+ app.should.be.called
666
+ response.should.be.redirect
667
+ response['Location'].should.equal '/'
668
+ cache.trace.should.include :invalidate
669
+ cache.trace.should.include :pass
670
+ response.body.should.equal ''
671
+
672
+ # now make sure it was actually invalidated
673
+ get '/'
674
+ app.should.be.called
675
+ response.should.be.ok
676
+ response.body.should.equal 'Hello World'
677
+ cache.trace.should.include :stale
678
+ cache.trace.should.include :invalid
679
+ cache.trace.should.include :store
680
+ end
681
+
682
+ describe 'with responses that include a Vary header' do
683
+ before(:each) do
684
+ count = 0
685
+ respond_with 200 do |req,res|
686
+ res['Vary'] = 'Accept User-Agent Foo'
687
+ res['Cache-Control'] = 'max-age=10'
688
+ res['X-Response-Count'] = (count+=1).to_s
689
+ res.body = [req.env['HTTP_USER_AGENT']]
690
+ end
691
+ end
692
+
693
+ it 'serves from cache when headers match' do
694
+ get '/',
695
+ 'HTTP_ACCEPT' => 'text/html',
696
+ 'HTTP_USER_AGENT' => 'Bob/1.0'
697
+ response.should.be.ok
698
+ response.body.should.equal 'Bob/1.0'
699
+ cache.trace.should.include :miss
700
+ cache.trace.should.include :store
701
+
702
+ get '/',
703
+ 'HTTP_ACCEPT' => 'text/html',
704
+ 'HTTP_USER_AGENT' => 'Bob/1.0'
705
+ response.should.be.ok
706
+ response.body.should.equal 'Bob/1.0'
707
+ cache.trace.should.include :fresh
708
+ cache.trace.should.not.include :store
709
+ response.headers.should.include 'X-Content-Digest'
710
+ end
711
+
712
+ it 'stores multiple responses when headers differ' do
713
+ get '/',
714
+ 'HTTP_ACCEPT' => 'text/html',
715
+ 'HTTP_USER_AGENT' => 'Bob/1.0'
716
+ response.should.be.ok
717
+ response.body.should.equal 'Bob/1.0'
718
+ response['X-Response-Count'].should.equal '1'
719
+
720
+ get '/',
721
+ 'HTTP_ACCEPT' => 'text/html',
722
+ 'HTTP_USER_AGENT' => 'Bob/2.0'
723
+ cache.trace.should.include :miss
724
+ cache.trace.should.include :store
725
+ response.body.should.equal 'Bob/2.0'
726
+ response['X-Response-Count'].should.equal '2'
727
+
728
+ get '/',
729
+ 'HTTP_ACCEPT' => 'text/html',
730
+ 'HTTP_USER_AGENT' => 'Bob/1.0'
731
+ cache.trace.should.include :fresh
732
+ response.body.should.equal 'Bob/1.0'
733
+ response['X-Response-Count'].should.equal '1'
734
+
735
+ get '/',
736
+ 'HTTP_ACCEPT' => 'text/html',
737
+ 'HTTP_USER_AGENT' => 'Bob/2.0'
738
+ cache.trace.should.include :fresh
739
+ response.body.should.equal 'Bob/2.0'
740
+ response['X-Response-Count'].should.equal '2'
741
+
742
+ get '/',
743
+ 'HTTP_USER_AGENT' => 'Bob/2.0'
744
+ cache.trace.should.include :miss
745
+ response.body.should.equal 'Bob/2.0'
746
+ response['X-Response-Count'].should.equal '3'
747
+ end
748
+ end
749
+
750
+ it 'passes if there was a metastore exception' do
751
+ respond_with 200, 'Cache-Control' => 'max-age=10000' do |req,res|
752
+ res.body = ['Hello World']
753
+ end
754
+
755
+ get '/'
756
+ response.should.be.ok
757
+ response.body.should.equal 'Hello World'
758
+ cache.trace.should.include :store
759
+
760
+ get '/' do |cache|
761
+ cache.meta_def(:metastore) { raise Timeout::Error }
762
+ end
763
+ response.should.be.ok
764
+ response.body.should.equal 'Hello World'
765
+ cache.trace.should.include :pass
766
+
767
+ post '/' do |cache|
768
+ cache.meta_def(:metastore) { raise Timeout::Error }
769
+ end
770
+ response.should.be.ok
771
+ response.body.should.equal 'Hello World'
772
+ cache.trace.should.include :pass
773
+ end
774
+ end