rtomayko-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.
Files changed (44) hide show
  1. data/CHANGES +50 -0
  2. data/COPYING +18 -0
  3. data/README +96 -0
  4. data/Rakefile +144 -0
  5. data/TODO +42 -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/config/busters.rb +16 -0
  15. data/lib/rack/cache/config/default.rb +134 -0
  16. data/lib/rack/cache/config/no-cache.rb +13 -0
  17. data/lib/rack/cache/config.rb +65 -0
  18. data/lib/rack/cache/context.rb +95 -0
  19. data/lib/rack/cache/core.rb +271 -0
  20. data/lib/rack/cache/entitystore.rb +224 -0
  21. data/lib/rack/cache/headers.rb +277 -0
  22. data/lib/rack/cache/metastore.rb +292 -0
  23. data/lib/rack/cache/options.rb +119 -0
  24. data/lib/rack/cache/request.rb +37 -0
  25. data/lib/rack/cache/response.rb +76 -0
  26. data/lib/rack/cache/storage.rb +50 -0
  27. data/lib/rack/cache.rb +51 -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 +505 -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 +222 -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 +122 -0
@@ -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,222 @@
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.httpdate(Time.now.httpdate)
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.httpdate(Time.now.httpdate)
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
+ it 'returns the correct date when the header is modified directly' do
93
+ @res = MockResponse.new(200, { 'Date' => @one_hour_ago.httpdate }, '')
94
+ @res.extend Rack::Cache::ResponseHeaders
95
+ @res.date.should.be == @one_hour_ago
96
+ @res.headers['Date'] = @now.httpdate
97
+ @res.date.should.be == @now
98
+ end
99
+ end
100
+
101
+ describe '#expires_at' do
102
+ it 'returns #date + #max_age when Cache-Control/max-age is present' do
103
+ @res.headers['Cache-Control'] = 'max-age=500'
104
+ @res.expires_at.should.be == @res.date + 500
105
+ end
106
+ it 'uses the Expires header when present and no Cache-Control/max-age' do
107
+ @res.headers['Expires'] = @one_hour_ago.httpdate
108
+ @res.expires_at.should.be == @one_hour_ago
109
+ end
110
+ it 'returns nil when no Expires or Cache-Control provided' do
111
+ @res.expires_at.should.be nil
112
+ end
113
+ end
114
+
115
+ describe '#max_age' do
116
+ it 'uses Cache-Control to calculate #max_age when present' do
117
+ @res.headers['Cache-Control'] = 'max-age=600'
118
+ @res.max_age.should.be == 600
119
+ end
120
+ it 'uses Expires for #max_age if no Cache-Control max-age present' do
121
+ @res.headers['Cache-Control'] = 'must-revalidate'
122
+ @res.headers['Expires'] = @one_hour_later.httpdate
123
+ @res.max_age.should.be == 60 ** 2
124
+ end
125
+ it 'gives a #max_age of nil when no freshness information available' do
126
+ @res.max_age.should.be.nil
127
+ end
128
+ end
129
+
130
+ describe '#freshness_information?' do
131
+ it 'is true when Expires header is present' do
132
+ @res.headers['Expires'] = Time.now.httpdate
133
+ @res.freshness_information?.should.be true
134
+ end
135
+ it 'is true when a Cache-Control max-age directive is present' do
136
+ @res.headers['Cache-Control'] = 'max-age=500'
137
+ @res.freshness_information?.should.be true
138
+ end
139
+ it 'is not true otherwise' do
140
+ @res.freshness_information?.should.be false
141
+ end
142
+ end
143
+
144
+ describe '#no_cache?' do
145
+ it 'is true when a Cache-Control no-cache directive is present' do
146
+ @res.headers['Cache-Control'] = 'no-cache'
147
+ @res.no_cache?.should.be true
148
+ end
149
+ it 'is false otherwise' do
150
+ @res.no_cache?.should.be false
151
+ end
152
+ end
153
+
154
+ describe '#stale?' do
155
+ it 'is true when TTL cannot be established' do
156
+ @res.should.be.stale
157
+ end
158
+ it 'is false when the TTL is <= 0' do
159
+ @res.headers['Expires'] = (@res.now + 10).httpdate
160
+ @res.should.not.be.stale
161
+ end
162
+ it 'is true when the TTL is >= 0' do
163
+ @res.headers['Expires'] = (@res.now - 10).httpdate
164
+ @res.should.be.stale
165
+ end
166
+ end
167
+
168
+ describe '#ttl' do
169
+ it 'is nil when no Expires or Cache-Control headers present' do
170
+ @res.ttl.should.be.nil
171
+ end
172
+ it 'uses the Expires header when no max-age is present' do
173
+ @res.headers['Expires'] = (@res.now + (60**2)).httpdate
174
+ @res.ttl.should.be.close(60**2, 1)
175
+ end
176
+ it 'returns negative values when Expires is in part' do
177
+ @res.ttl.should.be.nil
178
+ @res.headers['Expires'] = @one_hour_ago.httpdate
179
+ @res.ttl.should.be < 0
180
+ end
181
+ it 'uses the Cache-Control max-age value when present' do
182
+ @res.headers['Cache-Control'] = 'max-age=60'
183
+ @res.ttl.should.be.close(60, 1)
184
+ end
185
+ end
186
+
187
+ describe '#vary' do
188
+ it 'is nil when no Vary header is present' do
189
+ @res.vary.should.be.nil
190
+ end
191
+ it 'returns the literal value of the Vary header' do
192
+ @res.headers['Vary'] = 'Foo Bar Baz'
193
+ @res.vary.should.be == 'Foo Bar Baz'
194
+ end
195
+ it 'can be checked for existence using the #vary? method' do
196
+ @res.should.respond_to :vary?
197
+ @res.should.not.vary
198
+ @res.headers['Vary'] = '*'
199
+ @res.should.vary
200
+ end
201
+ end
202
+
203
+ describe '#vary_header_names' do
204
+ it 'returns an empty Array when no Vary header is present' do
205
+ @res.vary_header_names.should.be.empty
206
+ end
207
+ it 'parses a single header name value' do
208
+ @res.headers['Vary'] = 'Accept-Language'
209
+ @res.vary_header_names.should.be == ['Accept-Language']
210
+ end
211
+ it 'parses multiple header name values separated by spaces' do
212
+ @res.headers['Vary'] = 'Accept-Language User-Agent X-Foo'
213
+ @res.vary_header_names.should.be ==
214
+ ['Accept-Language', 'User-Agent', 'X-Foo']
215
+ end
216
+ it 'parses multiple header name values separated by commas' do
217
+ @res.headers['Vary'] = 'Accept-Language,User-Agent, X-Foo'
218
+ @res.vary_header_names.should.be ==
219
+ ['Accept-Language', 'User-Agent', 'X-Foo']
220
+ end
221
+ end
222
+ end
@@ -0,0 +1,45 @@
1
+ require "#{File.dirname(__FILE__)}/spec_setup"
2
+ require 'rack/cache/context'
3
+
4
+ describe "Rack::Cache::Context logging" do
5
+
6
+ before(:each) do
7
+ respond_with 200
8
+ @errors = StringIO.new
9
+ @cache = Rack::Cache::Context.new(@app)
10
+ @cache.errors = @errors
11
+ @cache.metaclass.send :public, :log, :trace, :warn, :info
12
+ end
13
+
14
+ it 'responds to #log by writing message to #errors' do
15
+ @cache.log :test, 'is this thing on?'
16
+ @errors.string.should.be == "[cache] test: is this thing on?\n"
17
+ end
18
+
19
+ it 'allows printf formatting arguments' do
20
+ @cache.log :test, '%s %p %i %x', 'hello', 'goodbye', 42, 66
21
+ @errors.string.should.be == "[cache] test: hello \"goodbye\" 42 42\n"
22
+ end
23
+
24
+ it 'responds to #info by logging an :info message' do
25
+ @cache.info 'informative stuff'
26
+ @errors.string.should.be == "[cache] info: informative stuff\n"
27
+ end
28
+
29
+ it 'responds to #warn by logging an :warn message' do
30
+ @cache.warn 'kinda/maybe bad stuff'
31
+ @errors.string.should.be == "[cache] warn: kinda/maybe bad stuff\n"
32
+ end
33
+
34
+ it 'responds to #trace by logging a :trace message' do
35
+ @cache.trace 'some insignifacant event'
36
+ @errors.string.should.be == "[cache] trace: some insignifacant event\n"
37
+ end
38
+
39
+ it "doesn't log trace messages when not in verbose mode" do
40
+ @cache.verbose = false
41
+ @cache.trace 'some insignifacant event'
42
+ @errors.string.should.be == ""
43
+ end
44
+
45
+ end