rack-cache 0.2.0

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.

Potentially problematic release.


This version of rack-cache might be problematic. Click here for more details.

Files changed (44) hide show
  1. data/CHANGES +27 -0
  2. data/COPYING +18 -0
  3. data/README +96 -0
  4. data/Rakefile +144 -0
  5. data/TODO +40 -0
  6. data/doc/configuration.markdown +224 -0
  7. data/doc/events.dot +27 -0
  8. data/doc/faq.markdown +133 -0
  9. data/doc/index.markdown +113 -0
  10. data/doc/layout.html.erb +33 -0
  11. data/doc/license.markdown +24 -0
  12. data/doc/rack-cache.css +362 -0
  13. data/doc/storage.markdown +162 -0
  14. data/lib/rack/cache.rb +51 -0
  15. data/lib/rack/cache/config.rb +65 -0
  16. data/lib/rack/cache/config/busters.rb +16 -0
  17. data/lib/rack/cache/config/default.rb +134 -0
  18. data/lib/rack/cache/config/no-cache.rb +13 -0
  19. data/lib/rack/cache/context.rb +95 -0
  20. data/lib/rack/cache/core.rb +271 -0
  21. data/lib/rack/cache/entitystore.rb +224 -0
  22. data/lib/rack/cache/headers.rb +237 -0
  23. data/lib/rack/cache/metastore.rb +309 -0
  24. data/lib/rack/cache/options.rb +119 -0
  25. data/lib/rack/cache/request.rb +37 -0
  26. data/lib/rack/cache/response.rb +76 -0
  27. data/lib/rack/cache/storage.rb +50 -0
  28. data/lib/rack/utils/environment_headers.rb +78 -0
  29. data/rack-cache.gemspec +74 -0
  30. data/test/cache_test.rb +35 -0
  31. data/test/config_test.rb +66 -0
  32. data/test/context_test.rb +465 -0
  33. data/test/core_test.rb +84 -0
  34. data/test/entitystore_test.rb +176 -0
  35. data/test/environment_headers_test.rb +71 -0
  36. data/test/headers_test.rb +215 -0
  37. data/test/logging_test.rb +45 -0
  38. data/test/metastore_test.rb +210 -0
  39. data/test/options_test.rb +64 -0
  40. data/test/pony.jpg +0 -0
  41. data/test/response_test.rb +37 -0
  42. data/test/spec_setup.rb +189 -0
  43. data/test/storage_test.rb +94 -0
  44. metadata +120 -0
@@ -0,0 +1,66 @@
1
+ require "#{File.dirname(__FILE__)}/spec_setup"
2
+ require 'rack/cache/config'
3
+
4
+ class MockConfig
5
+ include Rack::Cache::Config
6
+ def configured!
7
+ @configured = true
8
+ end
9
+ def configured?
10
+ @configured
11
+ end
12
+ end
13
+
14
+ describe 'Rack::Cache::Config' do
15
+ before :each do
16
+ @config = MockConfig.new
17
+ @tempdir = create_temp_directory
18
+ $:.unshift @tempdir
19
+ end
20
+ after :each do
21
+ @config = nil
22
+ $:.shift if $:.first == @tempdir
23
+ remove_entry_secure @tempdir
24
+ end
25
+
26
+ def make_temp_file(filename, data='configured!')
27
+ create_temp_file @tempdir, filename, data
28
+ end
29
+
30
+ it 'loads config files from the load path when file is relative' do
31
+ make_temp_file 'foo/bar.rb'
32
+ @config.import 'foo/bar.rb'
33
+ @config.should.be.configured
34
+ end
35
+ it 'assumes a .rb file extension when no file extension exists' do
36
+ make_temp_file 'foo/bar.rb'
37
+ @config.import 'foo/bar'
38
+ @config.should.be.configured
39
+ end
40
+ it 'does not assume a .rb file extension when other file extension exists' do
41
+ make_temp_file 'foo/bar.conf'
42
+ @config.import 'foo/bar.conf'
43
+ @config.should.be.configured
44
+ end
45
+ it 'should locate files with absolute path names' do
46
+ make_temp_file 'foo/bar.rb'
47
+ @config.import File.join(@tempdir, 'foo/bar.rb')
48
+ @config.should.be.configured
49
+ end
50
+ it 'raises a LoadError when the file cannot be found' do
51
+ assert_raises(LoadError) {
52
+ @config.import('this/file/is/very-likely/not/to/exist.rb')
53
+ }
54
+ end
55
+ it 'executes within the context of the object instance' do
56
+ make_temp_file 'foo/bar.rb',
57
+ 'self.should.be.kind_of Rack::Cache::Config ; configured!'
58
+ @config.import 'foo/bar'
59
+ @config.should.be.configured
60
+ end
61
+ it 'does not import files more than once' do
62
+ make_temp_file 'foo/bar.rb', "import 'foo/bar'"
63
+ @config.import('foo/bar').should.be true
64
+ @config.import('foo/bar').should.be false
65
+ end
66
+ end
@@ -0,0 +1,465 @@
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.should.a.performed :pass
15
+ response.headers.should.not.include 'Age'
16
+ end
17
+
18
+ it 'passes on requests with Authorization' do
19
+ respond_with 200
20
+ get '/', 'HTTP_AUTHORIZATION' => 'basic foobarbaz'
21
+
22
+ app.should.be.called
23
+ response.should.be.ok
24
+ cache.should.a.performed :pass
25
+ response.headers.should.not.include 'Age'
26
+ end
27
+
28
+ it 'passes on requests with a Cookie' do
29
+ respond_with 200
30
+ get '/', 'HTTP_COOKIE' => 'foo=bar'
31
+
32
+ response.should.be.ok
33
+ app.should.be.called
34
+ cache.should.a.performed :pass
35
+ response.headers.should.not.include 'Age'
36
+ end
37
+
38
+ it 'caches requests when Cache-Control request header set to no-cache' do
39
+ respond_with 200, 'Expires' => (Time.now + 5).httpdate
40
+ get '/', 'HTTP_CACHE_CONTROL' => 'no-cache'
41
+
42
+ response.should.be.ok
43
+ cache.should.a.performed :store
44
+ response.headers.should.not.include 'Age'
45
+ end
46
+
47
+ it 'fetches response from backend when cache misses' do
48
+ respond_with 200, 'Expires' => (Time.now + 5).httpdate
49
+ get '/'
50
+
51
+ response.should.be.ok
52
+ cache.should.a.performed :miss
53
+ cache.should.a.performed :fetch
54
+ response.headers.should.not.include 'Age'
55
+ end
56
+
57
+ [(201..202),(204..206),(303..305),(400..403),(405..409),(411..417),(500..505)].each do |range|
58
+ range.each do |response_code|
59
+ it "does not cache #{response_code} responses" do
60
+ respond_with response_code, 'Expires' => (Time.now + 5).httpdate
61
+ get '/'
62
+
63
+ cache.should.a.not.performed :store
64
+ response.status.should.be == response_code
65
+ response.headers.should.not.include 'Age'
66
+ end
67
+ end
68
+ end
69
+
70
+ it "does not cache responses with explicit no-store directive" do
71
+ respond_with 200,
72
+ 'Expires' => (Time.now + 5).httpdate,
73
+ 'Cache-Control' => 'no-store'
74
+ get '/'
75
+
76
+ response.should.be.ok
77
+ cache.should.a.not.performed :store
78
+ response.headers.should.not.include 'Age'
79
+ end
80
+
81
+ it 'does not cache responses without freshness information or a validator' do
82
+ respond_with 200
83
+ get '/'
84
+
85
+ response.should.be.ok
86
+ cache.should.a.not.performed :store
87
+ end
88
+
89
+ it "caches responses with explicit no-cache directive" do
90
+ respond_with 200,
91
+ 'Expires' => (Time.now + 5).httpdate,
92
+ 'Cache-Control' => 'no-cache'
93
+ get '/'
94
+
95
+ response.should.be.ok
96
+ cache.should.a.performed :store
97
+ response.headers.should.not.include 'Age'
98
+ end
99
+
100
+ it 'caches responses with an Expiration header' do
101
+ respond_with 200, 'Expires' => (Time.now + 5).httpdate
102
+ get '/'
103
+
104
+ response.should.be.ok
105
+ response.body.should.be == 'Hello World'
106
+ response.headers.should.include 'Date'
107
+ response['Age'].should.be.nil
108
+ response['X-Content-Digest'].should.be.nil
109
+ cache.should.a.performed :miss
110
+ cache.should.a.performed :store
111
+ cache.metastore.to_hash.keys.length.should.be == 1
112
+ end
113
+
114
+ it 'caches responses with a max-age directive' do
115
+ respond_with 200, 'Cache-Control' => 'max-age=5'
116
+ get '/'
117
+
118
+ response.should.be.ok
119
+ response.body.should.be == 'Hello World'
120
+ response.headers.should.include 'Date'
121
+ response['Age'].should.be.nil
122
+ response['X-Content-Digest'].should.be.nil
123
+ cache.should.a.performed :miss
124
+ cache.should.a.performed :store
125
+ cache.metastore.to_hash.keys.length.should.be == 1
126
+ end
127
+
128
+ it 'caches responses with a Last-Modified validator but no freshness information' do
129
+ respond_with 200, 'Last-Modified' => Time.now.httpdate
130
+ get '/'
131
+
132
+ response.should.be.ok
133
+ response.body.should.be == 'Hello World'
134
+ cache.should.a.performed :miss
135
+ cache.should.a.performed :store
136
+ end
137
+
138
+ it 'caches responses with an ETag validator but no freshness information' do
139
+ respond_with 200, 'Etag' => '"123456"'
140
+ get '/'
141
+
142
+ response.should.be.ok
143
+ response.body.should.be == 'Hello World'
144
+ cache.should.a.performed :miss
145
+ cache.should.a.performed :store
146
+ end
147
+
148
+ it 'hits cached response with Expires header' do
149
+ respond_with 200,
150
+ 'Date' => (Time.now - 5).httpdate,
151
+ 'Expires' => (Time.now + 5).httpdate
152
+
153
+ get '/'
154
+ app.should.be.called
155
+ response.should.be.ok
156
+ response.headers.should.include 'Date'
157
+ cache.should.a.performed :miss
158
+ cache.should.a.performed :store
159
+ response.body.should.be == 'Hello World'
160
+
161
+ get '/'
162
+ response.should.be.ok
163
+ app.should.not.be.called
164
+ response['Date'].should.be == responses.first['Date']
165
+ response['Age'].to_i.should.be > 0
166
+ response['X-Content-Digest'].should.not.be.nil
167
+ cache.should.a.performed :hit
168
+ cache.should.a.not.performed :fetch
169
+ response.body.should.be == 'Hello World'
170
+ end
171
+
172
+ it 'hits cached response with max-age directive' do
173
+ respond_with 200,
174
+ 'Date' => (Time.now - 5).httpdate,
175
+ 'Cache-Control' => 'max-age=10'
176
+
177
+ get '/'
178
+ app.should.be.called
179
+ response.should.be.ok
180
+ response.headers.should.include 'Date'
181
+ cache.should.a.performed :miss
182
+ cache.should.a.performed :store
183
+ response.body.should.be == 'Hello World'
184
+
185
+ get '/'
186
+ response.should.be.ok
187
+ app.should.not.be.called
188
+ response['Date'].should.be == responses.first['Date']
189
+ response['Age'].to_i.should.be > 0
190
+ response['X-Content-Digest'].should.not.be.nil
191
+ cache.should.a.performed :hit
192
+ cache.should.a.not.performed :fetch
193
+ response.body.should.be == 'Hello World'
194
+ end
195
+
196
+ it 'fetches full response when cache stale and no validators present' do
197
+ respond_with 200, 'Expires' => (Time.now + 5).httpdate
198
+
199
+ # build initial request
200
+ get '/'
201
+ app.should.be.called
202
+ response.should.be.ok
203
+ response.headers.should.include 'Date'
204
+ response.headers.should.not.include 'X-Content-Digest'
205
+ response['Age'].should.be.nil
206
+ cache.should.a.performed :miss
207
+ cache.should.a.performed :store
208
+ response.body.should.be == 'Hello World'
209
+
210
+ # go in and play around with the cached metadata directly ...
211
+ cache.metastore.to_hash.values.length.should.be == 1
212
+ cache.metastore.to_hash.values.first.first[1]['Expires'] = Time.now.httpdate
213
+
214
+ # build subsequent request; should be found but miss due to freshness
215
+ get '/'
216
+ app.should.be.called
217
+ response.should.be.ok
218
+ response['Age'].to_i.should.be == 0
219
+ response['X-Content-Digest'].should.be.nil
220
+ cache.should.a.not.performed :hit
221
+ cache.should.a.not.performed :miss
222
+ cache.should.a.performed :fetch
223
+ cache.should.a.performed :store
224
+ response.body.should.be == 'Hello World'
225
+ end
226
+
227
+ it 'validates cached responses with Last-Modified and no freshness information' do
228
+ timestamp = Time.now.httpdate
229
+ respond_with do |req,res|
230
+ res['Last-Modified'] = timestamp
231
+ if req.env['HTTP_IF_MODIFIED_SINCE'] == timestamp
232
+ res.status = 304
233
+ res.body = []
234
+ end
235
+ end
236
+
237
+ # build initial request
238
+ get '/'
239
+ app.should.be.called
240
+ response.should.be.ok
241
+ response.headers.should.include 'Last-Modified'
242
+ response.headers.should.not.include 'X-Content-Digest'
243
+ response.body.should.be == 'Hello World'
244
+ cache.should.a.performed :miss
245
+ cache.should.a.performed :store
246
+
247
+ # build subsequent request; should be found but miss due to freshness
248
+ get '/'
249
+ app.should.be.called
250
+ response.should.be.ok
251
+ response.headers.should.include 'Last-Modified'
252
+ response.headers.should.include 'X-Content-Digest'
253
+ response['Age'].to_i.should.be == 0
254
+ response['X-Origin-Status'].should.be == '304'
255
+ response.body.should.be == 'Hello World'
256
+ cache.should.a.not.performed :miss
257
+ cache.should.a.performed :fetch
258
+ cache.should.a.performed :store
259
+ end
260
+
261
+ it 'validates cached responses with ETag and no freshness information' do
262
+ timestamp = Time.now.httpdate
263
+ respond_with do |req,res|
264
+ res['ETAG'] = '"12345"'
265
+ if req.env['HTTP_IF_NONE_MATCH'] == res['Etag']
266
+ res.status = 304
267
+ res.body = []
268
+ end
269
+ end
270
+
271
+ # build initial request
272
+ get '/'
273
+ app.should.be.called
274
+ response.should.be.ok
275
+ response.headers.should.include 'Etag'
276
+ response.headers.should.not.include 'X-Content-Digest'
277
+ response.body.should.be == 'Hello World'
278
+ cache.should.a.performed :miss
279
+ cache.should.a.performed :store
280
+
281
+ # build subsequent request; should be found but miss due to freshness
282
+ get '/'
283
+ app.should.be.called
284
+ response.should.be.ok
285
+ response.headers.should.include 'Etag'
286
+ response.headers.should.include 'X-Content-Digest'
287
+ response['Age'].to_i.should.be == 0
288
+ response['X-Origin-Status'].should.be == '304'
289
+ response.body.should.be == 'Hello World'
290
+ cache.should.a.not.performed :miss
291
+ cache.should.a.performed :fetch
292
+ cache.should.a.performed :store
293
+ end
294
+
295
+ it 'replaces cached responses when validation results in non-304 response' do
296
+ timestamp = Time.now.httpdate
297
+ count = 0
298
+ respond_with do |req,res|
299
+ res['Last-Modified'] = timestamp
300
+ case (count+=1)
301
+ when 1 ; res.body = 'first response'
302
+ when 2 ; res.body = 'second response'
303
+ when 3
304
+ res.body = []
305
+ res.status = 304
306
+ end
307
+ end
308
+
309
+ # first request should fetch from backend and store in cache
310
+ get '/'
311
+ response.status.should.be == 200
312
+ response.body.should.be == 'first response'
313
+
314
+ # second request is validated, is invalid, and replaces cached entry
315
+ get '/'
316
+ response.status.should.be == 200
317
+ response.body.should.be == 'second response'
318
+
319
+ # third respone is validated, valid, and returns cached entry
320
+ get '/'
321
+ response.status.should.be == 200
322
+ response.body.should.be == 'second response'
323
+
324
+ count.should.be == 3
325
+ end
326
+
327
+ describe 'with responses that include a Vary header' do
328
+ before(:each) do
329
+ count = 0
330
+ respond_with 200 do |req,res|
331
+ res['Vary'] = 'Accept User-Agent Foo'
332
+ res['Cache-Control'] = 'max-age=10'
333
+ res['X-Response-Count'] = (count+=1).to_s
334
+ res.body = req.env['HTTP_USER_AGENT']
335
+ end
336
+ end
337
+
338
+ it 'serves from cache when headers match' do
339
+ get '/',
340
+ 'HTTP_ACCEPT' => 'text/html',
341
+ 'HTTP_USER_AGENT' => 'Bob/1.0'
342
+ response.should.be.ok
343
+ response.body.should.be == 'Bob/1.0'
344
+ cache.should.a.performed :miss
345
+ cache.should.a.performed :store
346
+
347
+ get '/',
348
+ 'HTTP_ACCEPT' => 'text/html',
349
+ 'HTTP_USER_AGENT' => 'Bob/1.0'
350
+ response.should.be.ok
351
+ response.body.should.be == 'Bob/1.0'
352
+ cache.should.a.performed :hit
353
+ cache.should.a.not.performed :fetch
354
+ response.headers.should.include 'X-Content-Digest'
355
+ end
356
+
357
+ it 'stores multiple responses when headers differ' do
358
+ get '/',
359
+ 'HTTP_ACCEPT' => 'text/html',
360
+ 'HTTP_USER_AGENT' => 'Bob/1.0'
361
+ response.should.be.ok
362
+ response.body.should.be == 'Bob/1.0'
363
+ response['X-Response-Count'].should.be == '1'
364
+
365
+ get '/',
366
+ 'HTTP_ACCEPT' => 'text/html',
367
+ 'HTTP_USER_AGENT' => 'Bob/2.0'
368
+ cache.should.a.performed :miss
369
+ cache.should.a.performed :store
370
+ response.body.should.be == 'Bob/2.0'
371
+ response['X-Response-Count'].should.be == '2'
372
+
373
+ get '/',
374
+ 'HTTP_ACCEPT' => 'text/html',
375
+ 'HTTP_USER_AGENT' => 'Bob/1.0'
376
+ cache.should.a.performed :hit
377
+ response.body.should.be == 'Bob/1.0'
378
+ response['X-Response-Count'].should.be == '1'
379
+
380
+ get '/',
381
+ 'HTTP_ACCEPT' => 'text/html',
382
+ 'HTTP_USER_AGENT' => 'Bob/2.0'
383
+ cache.should.a.performed :hit
384
+ response.body.should.be == 'Bob/2.0'
385
+ response['X-Response-Count'].should.be == '2'
386
+
387
+ get '/',
388
+ 'HTTP_USER_AGENT' => 'Bob/2.0'
389
+ cache.should.a.performed :miss
390
+ response.body.should.be == 'Bob/2.0'
391
+ response['X-Response-Count'].should.be == '3'
392
+ end
393
+ end
394
+
395
+ describe 'when transitioning to the error state' do
396
+
397
+ setup { respond_with(200) }
398
+
399
+ it 'creates a blank slate response object with 500 status with no args' do
400
+ cache_config do
401
+ on(:receive) { error! }
402
+ end
403
+ get '/'
404
+ response.status.should.be == 500
405
+ response.body.should.be.empty
406
+ cache.should.a.performed :error
407
+ end
408
+
409
+ it 'sets the status code with one arg' do
410
+ cache_config do
411
+ on(:receive) { error! 505 }
412
+ end
413
+ get '/'
414
+ response.status.should.be == 505
415
+ end
416
+
417
+ it 'sets the status and headers with args: status, Hash' do
418
+ cache_config do
419
+ on(:receive) { error! 504, 'Content-Type' => 'application/x-foo' }
420
+ end
421
+ get '/'
422
+ response.status.should.be == 504
423
+ response['Content-Type'].should.be == 'application/x-foo'
424
+ response.body.should.be.empty
425
+ end
426
+
427
+ it 'sets the status and body with args: status, String' do
428
+ cache_config do
429
+ on(:receive) { error! 503, 'foo bar baz' }
430
+ end
431
+ get '/'
432
+ response.status.should.be == 503
433
+ response.body.should.be == 'foo bar baz'
434
+ end
435
+
436
+ it 'sets the status and body with args: status, Array' do
437
+ cache_config do
438
+ on(:receive) { error! 503, ['foo bar baz'] }
439
+ end
440
+ get '/'
441
+ response.status.should.be == 503
442
+ response.body.should.be == 'foo bar baz'
443
+ end
444
+
445
+ it 'fires the error event before finishing' do
446
+ fired = false
447
+ cache_config do
448
+ on(:receive) { error! }
449
+ on(:error) {
450
+ fired = true
451
+ response.status.should.be == 500
452
+ response['Content-Type'] = 'application/x-foo'
453
+ response.body = ['overridden response body']
454
+ }
455
+ end
456
+ get '/'
457
+ fired.should.be true
458
+ response.status.should.be == 500
459
+ response.body.should.be == 'overridden response body'
460
+ response['Content-Type'].should.be == 'application/x-foo'
461
+ end
462
+
463
+ end
464
+
465
+ end