rack-cache 0.2.0

Sign up to get free protection for your applications and to get access to all the features.

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
data/test/core_test.rb ADDED
@@ -0,0 +1,84 @@
1
+ require "#{File.dirname(__FILE__)}/spec_setup"
2
+ require 'rack/cache/core'
3
+
4
+ class MockCore
5
+ include Rack::Cache::Core
6
+ alias_method :initialize, :initialize_core
7
+ public :transition, :trigger, :events
8
+ end
9
+
10
+ describe 'Rack::Cache::Core' do
11
+ before :each do
12
+ @core = MockCore.new
13
+ end
14
+
15
+ it 'has events after instantiation' do
16
+ @core.events.should.respond_to :[]
17
+ end
18
+ it 'defines and triggers event handlers' do
19
+ executed = false
20
+ @core.on(:foo) { executed = true }
21
+ @core.trigger :foo
22
+ executed.should.be true
23
+ end
24
+ it 'executes multiple handlers in LIFO order' do
25
+ x = 'nothing executed'
26
+ @core.on :foo do
27
+ x.should.be == 'bottom executed'
28
+ x = 'top executed'
29
+ end
30
+ @core.on :foo do
31
+ x.should.be == 'nothing executed'
32
+ x = 'bottom executed'
33
+ end
34
+ @core.trigger :foo
35
+ x.should.be == 'top executed'
36
+ end
37
+ it 'records event execution history' do
38
+ @core.on(:foo) {}
39
+ @core.trigger :foo
40
+ @core.should.a.performed :foo
41
+ end
42
+ it 'raises an exception when asked to perform an unknown event' do
43
+ assert_raises NameError do
44
+ @core.trigger :foo
45
+ end
46
+ end
47
+ it 'raises an exception when asked to transition to an unknown event' do
48
+ @core.on(:bling) {}
49
+ @core.on(:foo) { throw(:transition, [:bling]) }
50
+ lambda { @core.transition(from=:foo, to=[:bar, :baz]) }.
51
+ should.raise Rack::Cache::IllegalTransition
52
+ end
53
+ it 'passes transition arguments to handlers' do
54
+ passed = nil
55
+ @core.meta_def(:perform_bar) do |*args|
56
+ passed = args
57
+ 'hi'
58
+ end
59
+ @core.on(:bar) {}
60
+ @core.on(:foo) { throw(:transition, [:bar, 1, 2, 3]) }
61
+ result = @core.transition(from=:foo, to=[:bar])
62
+ passed.should.be == [1,2,3]
63
+ result.should.be == 'hi'
64
+ end
65
+ it 'fully transitions out of handlers when the next event is invoked' do
66
+ x = []
67
+ @core.on(:foo) {
68
+ x << 'in foo, before transitioning to bar'
69
+ throw(:transition, [:bar])
70
+ x << 'in foo, after transitioning to bar'
71
+ }
72
+ @core.on(:bar) { x << 'in bar' }
73
+ @core.trigger(:foo).should.be == [:bar]
74
+ @core.trigger(:bar).should.be.nil
75
+ x.should.be == [
76
+ 'in foo, before transitioning to bar',
77
+ 'in bar'
78
+ ]
79
+ end
80
+ it 'returns the transition event name' do
81
+ @core.on(:foo) { throw(:transition, [:bar]) }
82
+ @core.trigger(:foo).should.be == [:bar]
83
+ end
84
+ end
@@ -0,0 +1,176 @@
1
+ require "#{File.dirname(__FILE__)}/spec_setup"
2
+ require 'rack/cache/entitystore'
3
+
4
+ class Object
5
+ def sha_like?
6
+ length == 40 && self =~ /^[0-9a-z]+$/
7
+ end
8
+ end
9
+
10
+ describe_shared 'A Rack::Cache::EntityStore Implementation' do
11
+
12
+ it 'responds to all required messages' do
13
+ %w[read open write exist?].each do |message|
14
+ @store.should.respond_to message
15
+ end
16
+ end
17
+
18
+ it 'stores bodies with #write' do
19
+ key, size = @store.write('My wild love went riding,')
20
+ key.should.not.be.nil
21
+ key.should.be.sha_like
22
+
23
+ data = @store.read(key)
24
+ data.should.be == 'My wild love went riding,'
25
+ end
26
+
27
+ it 'correctly determines whether cached body exists for key with #exist?' do
28
+ key, size = @store.write('She rode to the devil,')
29
+ @store.should.exist key
30
+ @store.should.not.exist '938jasddj83jasdh4438021ksdfjsdfjsdsf'
31
+ end
32
+
33
+ it 'can read data written with #write' do
34
+ key, size = @store.write('And asked him to pay.')
35
+ data = @store.read(key)
36
+ data.should.be == 'And asked him to pay.'
37
+ end
38
+
39
+ it 'gives a 40 character SHA1 hex digest from #write' do
40
+ key, size = @store.write('she rode to the sea;')
41
+ key.should.not.be.nil
42
+ key.length.should.be == 40
43
+ key.should.be =~ /^[0-9a-z]+$/
44
+ key.should.be == '90a4c84d51a277f3dafc34693ca264531b9f51b6'
45
+ end
46
+
47
+ it 'returns the entire body as a String from #read' do
48
+ key, size = @store.write('She gathered together')
49
+ @store.read(key).should.be == 'She gathered together'
50
+ end
51
+
52
+ it 'returns nil from #read when key does not exist' do
53
+ @store.read('87fe0a1ae82a518592f6b12b0183e950b4541c62').should.be.nil
54
+ end
55
+
56
+ it 'returns a Rack compatible body from #open' do
57
+ key, size = @store.write('Some shells for her hair.')
58
+ body = @store.open(key)
59
+ body.should.respond_to :each
60
+ buf = ''
61
+ body.each { |part| buf << part }
62
+ buf.should.be == 'Some shells for her hair.'
63
+ end
64
+
65
+ it 'returns nil from #open when key does not exist' do
66
+ @store.open('87fe0a1ae82a518592f6b12b0183e950b4541c62').should.be.nil
67
+ end
68
+
69
+ it 'can store largish bodies with binary data' do
70
+ pony = File.read(File.dirname(__FILE__) + '/pony.jpg')
71
+ key, size = @store.write(pony)
72
+ key.should.be == 'd0f30d8659b4d268c5c64385d9790024c2d78deb'
73
+ data = @store.read(key)
74
+ data.length.should.be == pony.length
75
+ data.hash.should.be == pony.hash
76
+ end
77
+
78
+ end
79
+
80
+ describe 'Rack::Cache::EntityStore' do
81
+
82
+ describe 'Heap' do
83
+ it_should_behave_like 'A Rack::Cache::EntityStore Implementation'
84
+ before { @store = Rack::Cache::EntityStore::Heap.new }
85
+ it 'takes a Hash to ::new' do
86
+ @store = Rack::Cache::EntityStore::Heap.new('foo' => ['bar'])
87
+ @store.read('foo').should.be == 'bar'
88
+ end
89
+ it 'uses its own Hash with no args to ::new' do
90
+ @store.read('foo').should.be.nil
91
+ end
92
+ end
93
+
94
+ describe 'Disk' do
95
+ it_should_behave_like 'A Rack::Cache::EntityStore Implementation'
96
+ before do
97
+ @temp_dir = create_temp_directory
98
+ @store = Rack::Cache::EntityStore::Disk.new(@temp_dir)
99
+ end
100
+ after do
101
+ @store = nil
102
+ remove_entry_secure @temp_dir
103
+ end
104
+ it 'takes a path to ::new and creates the directory' do
105
+ path = @temp_dir + '/foo'
106
+ @store = Rack::Cache::EntityStore::Disk.new(path)
107
+ File.should.be.a.directory path
108
+ end
109
+ it 'spreads data over a 36² hash radius' do
110
+ (<<-PROSE).each { |line| @store.write(line).first.should.be.sha_like }
111
+ My wild love went riding,
112
+ She rode all the day;
113
+ She rode to the devil,
114
+ And asked him to pay.
115
+
116
+ The devil was wiser
117
+ It's time to repent;
118
+ He asked her to give back
119
+ The money she spent
120
+
121
+ My wild love went riding,
122
+ She rode to sea;
123
+ She gathered together
124
+ Some shells for her hair
125
+
126
+ She rode on to Christmas,
127
+ She rode to the farm;
128
+ She rode to Japan
129
+ And re-entered a town
130
+
131
+ My wild love is crazy
132
+ She screams like a bird;
133
+ She moans like a cat
134
+ When she wants to be heard
135
+
136
+ She rode and she rode on
137
+ She rode for a while,
138
+ Then stopped for an evening
139
+ And laid her head down
140
+
141
+ By this time the weather
142
+ Had changed one degree,
143
+ She asked for the people
144
+ To let her go free
145
+
146
+ My wild love went riding,
147
+ She rode for an hour;
148
+ She rode and she rested,
149
+ And then she rode on
150
+ My wild love went riding,
151
+ PROSE
152
+ subdirs = Dir["#{@temp_dir}/*"]
153
+ subdirs.each do |subdir|
154
+ File.basename(subdir).should.be =~ /^[0-9a-z]{2}$/
155
+ files = Dir["#{subdir}/*"]
156
+ files.each do |filename|
157
+ File.basename(filename).should.be =~ /^[0-9a-z]{38}$/
158
+ end
159
+ files.length.should.be > 0
160
+ end
161
+ subdirs.length.should.be == 28
162
+ end
163
+ end
164
+
165
+ need_memcached 'entity store tests' do
166
+ describe 'MemCache' do
167
+ it_should_behave_like 'A Rack::Cache::EntityStore Implementation'
168
+ before do
169
+ @store = Rack::Cache::EntityStore::MemCache.new($memcached)
170
+ end
171
+ after do
172
+ @store = nil
173
+ end
174
+ end
175
+ end
176
+ end
@@ -0,0 +1,71 @@
1
+ require "#{File.dirname(__FILE__)}/spec_setup"
2
+ require 'rack/utils/environment_headers'
3
+
4
+ describe 'Rack::Utils::EnvironmentHeaders' do
5
+
6
+ before :each do
7
+ @now = Time.now.httpdate
8
+ @env = {
9
+ 'CONTENT_TYPE' => 'text/plain',
10
+ 'CONTENT_LENGTH' => '0x1A4',
11
+ 'HTTP_X_FOO' => 'BAR',
12
+ 'HTTP_IF_MODIFIED_SINCE' => @now,
13
+ 'rack.run_once' => true
14
+ }
15
+ @h = Rack::Utils::EnvironmentHeaders.new(@env)
16
+ end
17
+
18
+ after(:each) {
19
+ @env, @h = nil, nil
20
+ }
21
+
22
+ it 'retrieves headers with #[]' do
23
+ @h.should.respond_to :[]
24
+ @h['X-Foo'].should.be == 'BAR'
25
+ @h['If-Modified-Since'].should.be == @now
26
+ end
27
+
28
+ it 'sets headers with #[]=' do
29
+ @h.should.respond_to :[]=
30
+ @h['X-Foo'] = 'BAZZLE'
31
+ @h['X-Foo'].should.be == 'BAZZLE'
32
+ end
33
+
34
+ it 'sets values on the underlying environment hash' do
35
+ @h['X-Something-Else'] = 'FOO'
36
+ @env['HTTP_X_SOMETHING_ELSE'].should.be == 'FOO'
37
+ end
38
+
39
+ it 'handles Content-Type special case' do
40
+ @h['Content-Type'].should.be == 'text/plain'
41
+ end
42
+
43
+ it 'handles Content-Length special case' do
44
+ @h['Content-Length'].should.be == '0x1A4'
45
+ end
46
+
47
+ it 'implements #include? with RFC 2616 header name' do
48
+ @h.should.include 'If-Modified-Since'
49
+ end
50
+
51
+ it 'deletes underlying env entries' do
52
+ @h.delete('X-Foo')
53
+ @env.should.not.include? 'HTTP_X_FOO'
54
+ end
55
+
56
+ it 'returns the underlying environment hash with #to_env' do
57
+ @h.to_env.should.be @env
58
+ end
59
+
60
+ it 'iterates over all headers with #each' do
61
+ hash = {}
62
+ @h.each { |name,value| hash[name] = value }
63
+ hash.should.be == {
64
+ 'Content-Type' => 'text/plain',
65
+ 'Content-Length' => '0x1A4',
66
+ 'X-Foo' => 'BAR',
67
+ 'If-Modified-Since' => @now
68
+ }
69
+ end
70
+
71
+ end
@@ -0,0 +1,215 @@
1
+ require "#{File.dirname(__FILE__)}/spec_setup"
2
+
3
+ class MockResponse < Rack::MockResponse
4
+ include Rack::Cache::Headers
5
+ include Rack::Cache::ResponseHeaders
6
+ public :now
7
+ end
8
+
9
+ describe 'Rack::Cache::Headers' do
10
+ before :each do
11
+ @now = Time.now
12
+ @res = MockResponse.new(200, {'Date' => @now.httpdate}, '')
13
+ @one_hour_ago = Time.httpdate((Time.now - (60**2)).httpdate)
14
+ end
15
+ after :each do
16
+ @now, @res, @one_hour_ago = nil
17
+ end
18
+
19
+ describe '#cache_control' do
20
+ it 'handles single name=value pair' do
21
+ @res.headers['Cache-Control'] = 'max-age=600'
22
+ @res.cache_control['max-age'].should.be == '600'
23
+ end
24
+ it 'handles multiple name=value pairs' do
25
+ @res.headers['Cache-Control'] = 'max-age=600, max-stale=300, min-fresh=570'
26
+ @res.cache_control['max-age'].should.be == '600'
27
+ @res.cache_control['max-stale'].should.be == '300'
28
+ @res.cache_control['min-fresh'].should.be == '570'
29
+ end
30
+ it 'handles a single flag value' do
31
+ @res.headers['Cache-Control'] = 'no-cache'
32
+ @res.cache_control.should.include 'no-cache'
33
+ @res.cache_control['no-cache'].should.be true
34
+ end
35
+ it 'handles a bunch of all kinds of stuff' do
36
+ @res.headers['Cache-Control'] = 'max-age=600,must-revalidate,min-fresh=3000,foo=bar,baz'
37
+ @res.cache_control['max-age'].should.be == '600'
38
+ @res.cache_control['must-revalidate'].should.be true
39
+ @res.cache_control['min-fresh'].should.be == '3000'
40
+ @res.cache_control['foo'].should.be == 'bar'
41
+ @res.cache_control['baz'].should.be true
42
+ end
43
+ it 'removes the header when given an empty hash' do
44
+ @res.headers['Cache-Control'] = 'max-age=600, must-revalidate'
45
+ @res.cache_control['max-age'].should.be == '600'
46
+ @res.cache_control = {}
47
+ @res.headers.should.not.include 'Cache-Control'
48
+ end
49
+ end
50
+ end
51
+
52
+ describe 'Rack::Cache::ResponseHeaders' do
53
+ before :each do
54
+ @now = Time.now
55
+ @one_hour_ago = Time.httpdate((Time.now - (60**2)).httpdate)
56
+ @one_hour_later = Time.httpdate((Time.now + (60**2)).httpdate)
57
+ @res = MockResponse.new(200, {'Date' => @now.httpdate}, '')
58
+ end
59
+ after :each do
60
+ @now, @res, @one_hour_ago = nil
61
+ end
62
+
63
+ describe '#validateable?' do
64
+ it 'is true when Last-Modified header present' do
65
+ @res = MockResponse.new(200, { 'Last-Modified' => @one_hour_ago.httpdate }, '')
66
+ @res.extend Rack::Cache::ResponseHeaders
67
+ @res.should.be.validateable
68
+ end
69
+ it 'is true when Etag header present' do
70
+ @res = MockResponse.new(200, { 'Etag' => '"12345"' }, '')
71
+ @res.extend Rack::Cache::ResponseHeaders
72
+ @res.should.be.validateable
73
+ end
74
+ it 'is false when no validator is present' do
75
+ @res = MockResponse.new(200, {}, '')
76
+ @res.extend Rack::Cache::ResponseHeaders
77
+ @res.should.not.be.validateable
78
+ end
79
+ end
80
+
81
+ describe '#date' do
82
+ it 'uses the Date header if present' do
83
+ @res = MockResponse.new(200, { 'Date' => @one_hour_ago.httpdate }, '')
84
+ @res.extend Rack::Cache::ResponseHeaders
85
+ @res.date.should.be == @one_hour_ago
86
+ end
87
+ it 'uses the current time when no Date header present' do
88
+ @res = MockResponse.new(200, {}, '')
89
+ @res.extend Rack::Cache::ResponseHeaders
90
+ @res.date.should.be.close Time.now, 1
91
+ end
92
+ end
93
+
94
+ describe '#expires_at' do
95
+ it 'returns #date + #max_age when Cache-Control/max-age is present' do
96
+ @res.headers['Cache-Control'] = 'max-age=500'
97
+ @res.expires_at.should.be == @res.date + 500
98
+ end
99
+ it 'uses the Expires header when present and no Cache-Control/max-age' do
100
+ @res.headers['Expires'] = @one_hour_ago.httpdate
101
+ @res.expires_at.should.be == @one_hour_ago
102
+ end
103
+ it 'returns nil when no Expires or Cache-Control provided' do
104
+ @res.expires_at.should.be nil
105
+ end
106
+ end
107
+
108
+ describe '#max_age' do
109
+ it 'uses Cache-Control to calculate #max_age when present' do
110
+ @res.headers['Cache-Control'] = 'max-age=600'
111
+ @res.max_age.should.be == 600
112
+ end
113
+ it 'uses Expires for #max_age if no Cache-Control max-age present' do
114
+ @res.headers['Cache-Control'] = 'must-revalidate'
115
+ @res.headers['Expires'] = @one_hour_later.httpdate
116
+ @res.max_age.should.be == 60 ** 2
117
+ end
118
+ it 'gives a #max_age of nil when no freshness information available' do
119
+ @res.max_age.should.be.nil
120
+ end
121
+ end
122
+
123
+ describe '#freshness_information?' do
124
+ it 'is true when Expires header is present' do
125
+ @res.headers['Expires'] = Time.now.httpdate
126
+ @res.freshness_information?.should.be true
127
+ end
128
+ it 'is true when a Cache-Control max-age directive is present' do
129
+ @res.headers['Cache-Control'] = 'max-age=500'
130
+ @res.freshness_information?.should.be true
131
+ end
132
+ it 'is not true otherwise' do
133
+ @res.freshness_information?.should.be false
134
+ end
135
+ end
136
+
137
+ describe '#no_cache?' do
138
+ it 'is true when a Cache-Control no-cache directive is present' do
139
+ @res.headers['Cache-Control'] = 'no-cache'
140
+ @res.no_cache?.should.be true
141
+ end
142
+ it 'is false otherwise' do
143
+ @res.no_cache?.should.be false
144
+ end
145
+ end
146
+
147
+ describe '#stale?' do
148
+ it 'is true when TTL cannot be established' do
149
+ @res.should.be.stale
150
+ end
151
+ it 'is false when the TTL is <= 0' do
152
+ @res.headers['Expires'] = (@res.now + 10).httpdate
153
+ @res.should.not.be.stale
154
+ end
155
+ it 'is true when the TTL is >= 0' do
156
+ @res.headers['Expires'] = (@res.now - 10).httpdate
157
+ @res.should.be.stale
158
+ end
159
+ end
160
+
161
+ describe '#ttl' do
162
+ it 'is nil when no Expires or Cache-Control headers present' do
163
+ @res.ttl.should.be.nil
164
+ end
165
+ it 'uses the Expires header when no max-age is present' do
166
+ @res.headers['Expires'] = (@res.now + (60**2)).httpdate
167
+ @res.ttl.should.be.close(60**2, 1)
168
+ end
169
+ it 'returns negative values when Expires is in part' do
170
+ @res.ttl.should.be.nil
171
+ @res.headers['Expires'] = @one_hour_ago.httpdate
172
+ @res.ttl.should.be < 0
173
+ end
174
+ it 'uses the Cache-Control max-age value when present' do
175
+ @res.headers['Cache-Control'] = 'max-age=60'
176
+ @res.ttl.should.be.close(60, 1)
177
+ end
178
+ end
179
+
180
+ describe '#vary' do
181
+ it 'is nil when no Vary header is present' do
182
+ @res.vary.should.be.nil
183
+ end
184
+ it 'returns the literal value of the Vary header' do
185
+ @res.headers['Vary'] = 'Foo Bar Baz'
186
+ @res.vary.should.be == 'Foo Bar Baz'
187
+ end
188
+ it 'can be checked for existence using the #vary? method' do
189
+ @res.should.respond_to :vary?
190
+ @res.should.not.vary
191
+ @res.headers['Vary'] = '*'
192
+ @res.should.vary
193
+ end
194
+ end
195
+
196
+ describe '#vary_header_names' do
197
+ it 'returns an empty Array when no Vary header is present' do
198
+ @res.vary_header_names.should.be.empty
199
+ end
200
+ it 'parses a single header name value' do
201
+ @res.headers['Vary'] = 'Accept-Language'
202
+ @res.vary_header_names.should.be == ['Accept-Language']
203
+ end
204
+ it 'parses multiple header name values separated by spaces' do
205
+ @res.headers['Vary'] = 'Accept-Language User-Agent X-Foo'
206
+ @res.vary_header_names.should.be ==
207
+ ['Accept-Language', 'User-Agent', 'X-Foo']
208
+ end
209
+ it 'parses multiple header name values separated by commas' do
210
+ @res.headers['Vary'] = 'Accept-Language,User-Agent, X-Foo'
211
+ @res.vary_header_names.should.be ==
212
+ ['Accept-Language', 'User-Agent', 'X-Foo']
213
+ end
214
+ end
215
+ end