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,230 @@
1
+ # coding: utf-8
2
+ require "#{File.dirname(__FILE__)}/spec_setup"
3
+ require 'rack/cache/entitystore'
4
+
5
+ class Object
6
+ def sha_like?
7
+ length == 40 && self =~ /^[0-9a-z]+$/
8
+ end
9
+ end
10
+
11
+ describe_shared 'A Rack::Cache::EntityStore Implementation' do
12
+
13
+ it 'responds to all required messages' do
14
+ %w[read open write exist?].each do |message|
15
+ @store.should.respond_to message
16
+ end
17
+ end
18
+
19
+ it 'stores bodies with #write' do
20
+ key, size = @store.write(['My wild love went riding,'])
21
+ key.should.not.be.nil
22
+ key.should.be.sha_like
23
+
24
+ data = @store.read(key)
25
+ data.should.equal 'My wild love went riding,'
26
+ end
27
+
28
+ it 'correctly determines whether cached body exists for key with #exist?' do
29
+ key, size = @store.write(['She rode to the devil,'])
30
+ @store.should.exist key
31
+ @store.should.not.exist '938jasddj83jasdh4438021ksdfjsdfjsdsf'
32
+ end
33
+
34
+ it 'can read data written with #write' do
35
+ key, size = @store.write(['And asked him to pay.'])
36
+ data = @store.read(key)
37
+ data.should.equal 'And asked him to pay.'
38
+ end
39
+
40
+ it 'gives a 40 character SHA1 hex digest from #write' do
41
+ key, size = @store.write(['she rode to the sea;'])
42
+ key.should.not.be.nil
43
+ key.length.should.equal 40
44
+ key.should.be =~ /^[0-9a-z]+$/
45
+ key.should.equal '90a4c84d51a277f3dafc34693ca264531b9f51b6'
46
+ end
47
+
48
+ it 'returns the entire body as a String from #read' do
49
+ key, size = @store.write(['She gathered together'])
50
+ @store.read(key).should.equal 'She gathered together'
51
+ end
52
+
53
+ it 'returns nil from #read when key does not exist' do
54
+ @store.read('87fe0a1ae82a518592f6b12b0183e950b4541c62').should.be.nil
55
+ end
56
+
57
+ it 'returns a Rack compatible body from #open' do
58
+ key, size = @store.write(['Some shells for her hair.'])
59
+ body = @store.open(key)
60
+ body.should.respond_to :each
61
+ buf = ''
62
+ body.each { |part| buf << part }
63
+ buf.should.equal 'Some shells for her hair.'
64
+ end
65
+
66
+ it 'returns nil from #open when key does not exist' do
67
+ @store.open('87fe0a1ae82a518592f6b12b0183e950b4541c62').should.be.nil
68
+ end
69
+
70
+ it 'can store largish bodies with binary data' do
71
+ pony = File.open(File.dirname(__FILE__) + '/pony.jpg', 'rb') { |f| f.read }
72
+ key, size = @store.write([pony])
73
+ key.should.equal 'd0f30d8659b4d268c5c64385d9790024c2d78deb'
74
+ data = @store.read(key)
75
+ data.length.should.equal pony.length
76
+ data.hash.should.equal pony.hash
77
+ end
78
+
79
+ it 'deletes stored entries with #purge' do
80
+ key, size = @store.write(['My wild love went riding,'])
81
+ @store.purge(key).should.be.nil
82
+ @store.read(key).should.be.nil
83
+ end
84
+ end
85
+
86
+ describe 'Rack::Cache::EntityStore' do
87
+
88
+ describe 'Heap' do
89
+ it_should_behave_like 'A Rack::Cache::EntityStore Implementation'
90
+ before { @store = Rack::Cache::EntityStore::Heap.new }
91
+ it 'takes a Hash to ::new' do
92
+ @store = Rack::Cache::EntityStore::Heap.new('foo' => ['bar'])
93
+ @store.read('foo').should.equal 'bar'
94
+ end
95
+ it 'uses its own Hash with no args to ::new' do
96
+ @store.read('foo').should.be.nil
97
+ end
98
+ end
99
+
100
+ describe 'Disk' do
101
+ it_should_behave_like 'A Rack::Cache::EntityStore Implementation'
102
+ before do
103
+ @temp_dir = create_temp_directory
104
+ @store = Rack::Cache::EntityStore::Disk.new(@temp_dir)
105
+ end
106
+ after do
107
+ @store = nil
108
+ remove_entry_secure @temp_dir
109
+ end
110
+ it 'takes a path to ::new and creates the directory' do
111
+ path = @temp_dir + '/foo'
112
+ @store = Rack::Cache::EntityStore::Disk.new(path)
113
+ File.should.be.a.directory path
114
+ end
115
+ it 'produces a body that responds to #to_path' do
116
+ key, size = @store.write(['Some shells for her hair.'])
117
+ body = @store.open(key)
118
+ body.should.respond_to :to_path
119
+ path = "#{@temp_dir}/#{key[0..1]}/#{key[2..-1]}"
120
+ body.to_path.should.equal path
121
+ end
122
+ it 'spreads data over a 36² hash radius' do
123
+ (<<-PROSE).each_line { |line| @store.write([line]).first.should.be.sha_like }
124
+ My wild love went riding,
125
+ She rode all the day;
126
+ She rode to the devil,
127
+ And asked him to pay.
128
+
129
+ The devil was wiser
130
+ It's time to repent;
131
+ He asked her to give back
132
+ The money she spent
133
+
134
+ My wild love went riding,
135
+ She rode to sea;
136
+ She gathered together
137
+ Some shells for her hair
138
+
139
+ She rode on to Christmas,
140
+ She rode to the farm;
141
+ She rode to Japan
142
+ And re-entered a town
143
+
144
+ My wild love is crazy
145
+ She screams like a bird;
146
+ She moans like a cat
147
+ When she wants to be heard
148
+
149
+ She rode and she rode on
150
+ She rode for a while,
151
+ Then stopped for an evening
152
+ And laid her head down
153
+
154
+ By this time the weather
155
+ Had changed one degree,
156
+ She asked for the people
157
+ To let her go free
158
+
159
+ My wild love went riding,
160
+ She rode for an hour;
161
+ She rode and she rested,
162
+ And then she rode on
163
+ My wild love went riding,
164
+ PROSE
165
+ subdirs = Dir["#{@temp_dir}/*"]
166
+ subdirs.each do |subdir|
167
+ File.basename(subdir).should.be =~ /^[0-9a-z]{2}$/
168
+ files = Dir["#{subdir}/*"]
169
+ files.each do |filename|
170
+ File.basename(filename).should.be =~ /^[0-9a-z]{38}$/
171
+ end
172
+ files.length.should.be > 0
173
+ end
174
+ subdirs.length.should.equal 28
175
+ end
176
+ end
177
+
178
+ need_memcached 'entity store tests' do
179
+ describe 'MemCached' do
180
+ it_should_behave_like 'A Rack::Cache::EntityStore Implementation'
181
+ before do
182
+ @store = Rack::Cache::EntityStore::MemCached.new($memcached)
183
+ end
184
+ after do
185
+ @store = nil
186
+ end
187
+ end
188
+ end
189
+
190
+
191
+ need_memcache 'entity store tests' do
192
+ describe 'MemCache' do
193
+ it_should_behave_like 'A Rack::Cache::EntityStore Implementation'
194
+ before do
195
+ $memcache.flush_all
196
+ @store = Rack::Cache::EntityStore::MemCache.new($memcache)
197
+ end
198
+ after do
199
+ @store = nil
200
+ end
201
+ end
202
+ end
203
+
204
+ need_java 'entity store testing' do
205
+ module Rack::Cache::AppEngine
206
+ module MC
207
+ class << (Service = {})
208
+
209
+ def contains(key); include?(key); end
210
+ def get(key); self[key]; end;
211
+ def put(key, value, ttl = nil)
212
+ self[key] = value
213
+ end
214
+ end
215
+
216
+ end
217
+ end
218
+
219
+ describe 'GAEStore' do
220
+ it_should_behave_like 'A Rack::Cache::EntityStore Implementation'
221
+ before do
222
+ puts Rack::Cache::AppEngine::MC::Service.inspect
223
+ @store = Rack::Cache::EntityStore::GAEStore.new
224
+ end
225
+ after do
226
+ @store = nil
227
+ end
228
+ end
229
+ end
230
+ end
@@ -0,0 +1,50 @@
1
+ require "#{File.dirname(__FILE__)}/spec_setup"
2
+ require 'rack/cache/key'
3
+
4
+ describe 'A Rack::Cache::Key' do
5
+ it "sorts params" do
6
+ request = mock_request('/test?z=last&a=first')
7
+ new_key(request).should.include('a=first&z=last')
8
+ end
9
+
10
+ it "includes the scheme" do
11
+ request = mock_request(
12
+ '/test',
13
+ 'rack.url_scheme' => 'https',
14
+ 'HTTP_HOST' => 'www2.example.org'
15
+ )
16
+ new_key(request).should.include('https://')
17
+ end
18
+
19
+ it "includes host" do
20
+ request = mock_request('/test', "HTTP_HOST" => 'www2.example.org')
21
+ new_key(request).should.include('www2.example.org')
22
+ end
23
+
24
+ it "includes path" do
25
+ request = mock_request('/test')
26
+ new_key(request).should.include('/test')
27
+ end
28
+
29
+ it "sorts the query string by key/value after decoding" do
30
+ request = mock_request('/test?x=q&a=b&%78=c')
31
+ new_key(request).should.match(/\?a=b&x=c&x=q$/)
32
+ end
33
+
34
+ it "is in order of scheme, host, path, params" do
35
+ request = mock_request('/test?x=y', "HTTP_HOST" => 'www2.example.org')
36
+ new_key(request).should.equal "http://www2.example.org/test?x=y"
37
+ end
38
+
39
+ # Helper Methods =============================================================
40
+
41
+ define_method :mock_request do |*args|
42
+ uri, opts = args
43
+ env = Rack::MockRequest.env_for(uri, opts || {})
44
+ Rack::Cache::Request.new(env)
45
+ end
46
+
47
+ define_method :new_key do |request|
48
+ Rack::Cache::Key.call(request)
49
+ end
50
+ end
@@ -0,0 +1,302 @@
1
+ require "#{File.dirname(__FILE__)}/spec_setup"
2
+ require 'rack/cache/metastore'
3
+
4
+ describe_shared 'A Rack::Cache::MetaStore Implementation' do
5
+ before do
6
+ @request = mock_request('/', {})
7
+ @response = mock_response(200, {}, ['hello world'])
8
+ @entity_store = nil
9
+ end
10
+ after do
11
+ @store = nil
12
+ @entity_store = nil
13
+ end
14
+
15
+ # Low-level implementation methods ===========================================
16
+
17
+ it 'writes a list of negotation tuples with #write' do
18
+ lambda { @store.write('/test', [[{}, {}]]) }.should.not.raise
19
+ end
20
+
21
+ it 'reads a list of negotation tuples with #read' do
22
+ @store.write('/test', [[{},{}],[{},{}]])
23
+ tuples = @store.read('/test')
24
+ tuples.should.equal [ [{},{}], [{},{}] ]
25
+ end
26
+
27
+ it 'reads an empty list with #read when nothing cached at key' do
28
+ @store.read('/nothing').should.be.empty
29
+ end
30
+
31
+ it 'removes entries for key with #purge' do
32
+ @store.write('/test', [[{},{}]])
33
+ @store.read('/test').should.not.be.empty
34
+
35
+ @store.purge('/test')
36
+ @store.read('/test').should.be.empty
37
+ end
38
+
39
+ it 'succeeds when purging non-existing entries' do
40
+ @store.read('/test').should.be.empty
41
+ @store.purge('/test')
42
+ end
43
+
44
+ it 'returns nil from #purge' do
45
+ @store.write('/test', [[{},{}]])
46
+ @store.purge('/test').should.be nil
47
+ @store.read('/test').should.equal []
48
+ end
49
+
50
+ %w[/test http://example.com:8080/ /test?x=y /test?x=y&p=q].each do |key|
51
+ it "can read and write key: '#{key}'" do
52
+ lambda { @store.write(key, [[{},{}]]) }.should.not.raise
53
+ @store.read(key).should.equal [[{},{}]]
54
+ end
55
+ end
56
+
57
+ it "can read and write fairly large keys" do
58
+ key = "b" * 4096
59
+ lambda { @store.write(key, [[{},{}]]) }.should.not.raise
60
+ @store.read(key).should.equal [[{},{}]]
61
+ end
62
+
63
+ it "allows custom cache keys from block" do
64
+ request = mock_request('/test', {})
65
+ request.env['rack-cache.cache_key'] =
66
+ lambda { |request| request.path_info.reverse }
67
+ @store.cache_key(request).should == 'tset/'
68
+ end
69
+
70
+ it "allows custom cache keys from class" do
71
+ request = mock_request('/test', {})
72
+ request.env['rack-cache.cache_key'] = Class.new do
73
+ def self.call(request); request.path_info.reverse end
74
+ end
75
+ @store.cache_key(request).should == 'tset/'
76
+ end
77
+
78
+ # Abstract methods ===========================================================
79
+
80
+ # Stores an entry for the given request args, returns a url encoded cache key
81
+ # for the request.
82
+ define_method :store_simple_entry do |*request_args|
83
+ path, headers = request_args
84
+ @request = mock_request(path || '/test', headers || {})
85
+ @response = mock_response(200, {'Cache-Control' => 'max-age=420'}, ['test'])
86
+ body = @response.body
87
+ cache_key = @store.store(@request, @response, @entity_store)
88
+ @response.body.should.not.be body
89
+ cache_key
90
+ end
91
+
92
+ it 'stores a cache entry' do
93
+ cache_key = store_simple_entry
94
+ @store.read(cache_key).should.not.be.empty
95
+ end
96
+
97
+ it 'sets the X-Content-Digest response header before storing' do
98
+ cache_key = store_simple_entry
99
+ req, res = @store.read(cache_key).first
100
+ res['X-Content-Digest'].should.equal 'a94a8fe5ccb19ba61c4c0873d391e987982fbbd3'
101
+ end
102
+
103
+ it 'finds a stored entry with #lookup' do
104
+ store_simple_entry
105
+ response = @store.lookup(@request, @entity_store)
106
+ response.should.not.be.nil
107
+ response.should.be.kind_of Rack::Cache::Response
108
+ end
109
+
110
+ it 'does not find an entry with #lookup when none exists' do
111
+ req = mock_request('/test', {'HTTP_FOO' => 'Foo', 'HTTP_BAR' => 'Bar'})
112
+ @store.lookup(req, @entity_store).should.be.nil
113
+ end
114
+
115
+ it "canonizes urls for cache keys" do
116
+ store_simple_entry(path='/test?x=y&p=q')
117
+
118
+ hits_req = mock_request(path, {})
119
+ miss_req = mock_request('/test?p=x', {})
120
+
121
+ @store.lookup(hits_req, @entity_store).should.not.be.nil
122
+ @store.lookup(miss_req, @entity_store).should.be.nil
123
+ end
124
+
125
+ it 'does not find an entry with #lookup when the body does not exist' do
126
+ store_simple_entry
127
+ @response.headers['X-Content-Digest'].should.not.be.nil
128
+ @entity_store.purge(@response.headers['X-Content-Digest'])
129
+ @store.lookup(@request, @entity_store).should.be.nil
130
+ end
131
+
132
+ it 'restores response headers properly with #lookup' do
133
+ store_simple_entry
134
+ response = @store.lookup(@request, @entity_store)
135
+ response.headers.
136
+ should.equal @response.headers.merge('Content-Length' => '4')
137
+ end
138
+
139
+ it 'restores response body from entity store with #lookup' do
140
+ store_simple_entry
141
+ response = @store.lookup(@request, @entity_store)
142
+ body = '' ; response.body.each {|p| body << p}
143
+ body.should.equal 'test'
144
+ end
145
+
146
+ it 'invalidates meta and entity store entries with #invalidate' do
147
+ store_simple_entry
148
+ @store.invalidate(@request, @entity_store)
149
+ response = @store.lookup(@request, @entity_store)
150
+ response.should.be.kind_of Rack::Cache::Response
151
+ response.should.not.be.fresh
152
+ end
153
+
154
+ it 'succeeds quietly when #invalidate called with no matching entries' do
155
+ req = mock_request('/test', {})
156
+ @store.invalidate(req, @entity_store)
157
+ @store.lookup(@request, @entity_store).should.be.nil
158
+ end
159
+
160
+ # Vary =======================================================================
161
+
162
+ it 'does not return entries that Vary with #lookup' do
163
+ req1 = mock_request('/test', {'HTTP_FOO' => 'Foo', 'HTTP_BAR' => 'Bar'})
164
+ req2 = mock_request('/test', {'HTTP_FOO' => 'Bling', 'HTTP_BAR' => 'Bam'})
165
+ res = mock_response(200, {'Vary' => 'Foo Bar'}, ['test'])
166
+ @store.store(req1, res, @entity_store)
167
+
168
+ @store.lookup(req2, @entity_store).should.be.nil
169
+ end
170
+
171
+ it 'stores multiple responses for each Vary combination' do
172
+ req1 = mock_request('/test', {'HTTP_FOO' => 'Foo', 'HTTP_BAR' => 'Bar'})
173
+ res1 = mock_response(200, {'Vary' => 'Foo Bar'}, ['test 1'])
174
+ key = @store.store(req1, res1, @entity_store)
175
+
176
+ req2 = mock_request('/test', {'HTTP_FOO' => 'Bling', 'HTTP_BAR' => 'Bam'})
177
+ res2 = mock_response(200, {'Vary' => 'Foo Bar'}, ['test 2'])
178
+ @store.store(req2, res2, @entity_store)
179
+
180
+ req3 = mock_request('/test', {'HTTP_FOO' => 'Baz', 'HTTP_BAR' => 'Boom'})
181
+ res3 = mock_response(200, {'Vary' => 'Foo Bar'}, ['test 3'])
182
+ @store.store(req3, res3, @entity_store)
183
+
184
+ slurp(@store.lookup(req3, @entity_store).body).should.equal 'test 3'
185
+ slurp(@store.lookup(req1, @entity_store).body).should.equal 'test 1'
186
+ slurp(@store.lookup(req2, @entity_store).body).should.equal 'test 2'
187
+
188
+ @store.read(key).length.should.equal 3
189
+ end
190
+
191
+ it 'overwrites non-varying responses with #store' do
192
+ req1 = mock_request('/test', {'HTTP_FOO' => 'Foo', 'HTTP_BAR' => 'Bar'})
193
+ res1 = mock_response(200, {'Vary' => 'Foo Bar'}, ['test 1'])
194
+ key = @store.store(req1, res1, @entity_store)
195
+ slurp(@store.lookup(req1, @entity_store).body).should.equal 'test 1'
196
+
197
+ req2 = mock_request('/test', {'HTTP_FOO' => 'Bling', 'HTTP_BAR' => 'Bam'})
198
+ res2 = mock_response(200, {'Vary' => 'Foo Bar'}, ['test 2'])
199
+ @store.store(req2, res2, @entity_store)
200
+ slurp(@store.lookup(req2, @entity_store).body).should.equal 'test 2'
201
+
202
+ req3 = mock_request('/test', {'HTTP_FOO' => 'Foo', 'HTTP_BAR' => 'Bar'})
203
+ res3 = mock_response(200, {'Vary' => 'Foo Bar'}, ['test 3'])
204
+ @store.store(req3, res3, @entity_store)
205
+ slurp(@store.lookup(req1, @entity_store).body).should.equal 'test 3'
206
+
207
+ @store.read(key).length.should.equal 2
208
+ end
209
+
210
+ # Helper Methods =============================================================
211
+
212
+ define_method :mock_request do |uri,opts|
213
+ env = Rack::MockRequest.env_for(uri, opts || {})
214
+ Rack::Cache::Request.new(env)
215
+ end
216
+
217
+ define_method :mock_response do |status,headers,body|
218
+ headers ||= {}
219
+ body = Array(body).compact
220
+ Rack::Cache::Response.new(status, headers, body)
221
+ end
222
+
223
+ define_method :slurp do |body|
224
+ buf = ''
225
+ body.each {|part| buf << part }
226
+ buf
227
+ end
228
+ end
229
+
230
+
231
+ describe 'Rack::Cache::MetaStore' do
232
+ describe 'Heap' do
233
+ it_should_behave_like 'A Rack::Cache::MetaStore Implementation'
234
+ before do
235
+ @store = Rack::Cache::MetaStore::Heap.new
236
+ @entity_store = Rack::Cache::EntityStore::Heap.new
237
+ end
238
+ end
239
+
240
+ describe 'Disk' do
241
+ it_should_behave_like 'A Rack::Cache::MetaStore Implementation'
242
+ before do
243
+ @temp_dir = create_temp_directory
244
+ @store = Rack::Cache::MetaStore::Disk.new("#{@temp_dir}/meta")
245
+ @entity_store = Rack::Cache::EntityStore::Disk.new("#{@temp_dir}/entity")
246
+ end
247
+ after do
248
+ remove_entry_secure @temp_dir
249
+ end
250
+ end
251
+
252
+ need_memcached 'metastore tests' do
253
+ describe 'MemCached' do
254
+ it_should_behave_like 'A Rack::Cache::MetaStore Implementation'
255
+ before :each do
256
+ @temp_dir = create_temp_directory
257
+ $memcached.flush
258
+ @store = Rack::Cache::MetaStore::MemCached.new($memcached)
259
+ @entity_store = Rack::Cache::EntityStore::Heap.new
260
+ end
261
+ end
262
+ end
263
+
264
+ need_memcache 'metastore tests' do
265
+ describe 'MemCache' do
266
+ it_should_behave_like 'A Rack::Cache::MetaStore Implementation'
267
+ before :each do
268
+ @temp_dir = create_temp_directory
269
+ $memcache.flush_all
270
+ @store = Rack::Cache::MetaStore::MemCache.new($memcache)
271
+ @entity_store = Rack::Cache::EntityStore::Heap.new
272
+ end
273
+ end
274
+ end
275
+
276
+ need_java 'entity store testing' do
277
+ module Rack::Cache::AppEngine
278
+ module MC
279
+ class << (Service = {})
280
+
281
+ def contains(key); include?(key); end
282
+ def get(key); self[key]; end;
283
+ def put(key, value, ttl = nil)
284
+ self[key] = value
285
+ end
286
+
287
+ end
288
+ end
289
+ end
290
+
291
+ describe 'GAEStore' do
292
+ it_should_behave_like 'A Rack::Cache::MetaStore Implementation'
293
+ before :each do
294
+ Rack::Cache::AppEngine::MC::Service.clear
295
+ @store = Rack::Cache::MetaStore::GAEStore.new
296
+ @entity_store = Rack::Cache::EntityStore::Heap.new
297
+ end
298
+ end
299
+
300
+ end
301
+
302
+ end