josh-rack-cache 0.5.1

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,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