dsander-redis-store 0.3.8

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,257 @@
1
+ require File.join(File.dirname(__FILE__), "/../../../spec_helper")
2
+
3
+ module Rack
4
+ module Cache
5
+ class MetaStore
6
+ # courtesy of http://github.com/rtomayko/rack-cache team
7
+ describe "Rack::Cache::MetaStore::Redis" do
8
+ before :each do
9
+ @store = Rack::Cache::MetaStore::Redis.resolve uri("redis://127.0.0.1")
10
+ @entity_store = Rack::Cache::EntityStore::Redis.resolve uri("redis://127.0.0.1:6380")
11
+ @request = mock_request('/', {})
12
+ @response = mock_response(200, {}, ['hello world'])
13
+ end
14
+
15
+ after :each do
16
+ @store.cache.flushall
17
+ @entity_store.cache.flushall
18
+ end
19
+
20
+ it "should have the class referenced by homonym constant" do
21
+ Rack::Cache::MetaStore::REDIS.should be(Rack::Cache::MetaStore::Redis)
22
+ end
23
+
24
+ it "should resolve the connection uri" do
25
+ cache = Rack::Cache::MetaStore::Redis.resolve(uri("redis://127.0.0.1")).cache
26
+ cache.should be_kind_of(::MarshaledRedis)
27
+ cache.host.should == "127.0.0.1"
28
+ cache.port.should == 6379
29
+ cache.db.should == 0
30
+
31
+ cache = Rack::Cache::MetaStore::Redis.resolve(uri("redis://127.0.0.1:6380")).cache
32
+ cache.port.should == 6380
33
+
34
+ cache = Rack::Cache::MetaStore::Redis.resolve(uri("redis://127.0.0.1/13")).cache
35
+ cache.db.should == 13
36
+ end
37
+
38
+ # Low-level implementation methods ===========================================
39
+
40
+ it 'writes a list of negotation tuples with #write' do
41
+ lambda { @store.write('/test', [[{}, {}]]) }.should_not raise_error
42
+ end
43
+
44
+ it 'reads a list of negotation tuples with #read' do
45
+ @store.write('/test', [[{},{}],[{},{}]])
46
+ tuples = @store.read('/test')
47
+ tuples.should == [ [{},{}], [{},{}] ]
48
+ end
49
+
50
+ it 'reads an empty list with #read when nothing cached at key' do
51
+ @store.read('/nothing').should be_empty
52
+ end
53
+
54
+ it 'removes entries for key with #purge' do
55
+ @store.write('/test', [[{},{}]])
56
+ @store.read('/test').should_not be_empty
57
+
58
+ @store.purge('/test')
59
+ @store.read('/test').should be_empty
60
+ end
61
+
62
+ it 'succeeds when purging non-existing entries' do
63
+ @store.read('/test').should be_empty
64
+ @store.purge('/test')
65
+ end
66
+
67
+ it 'returns nil from #purge' do
68
+ @store.write('/test', [[{},{}]])
69
+ @store.purge('/test').should be_nil
70
+ @store.read('/test').should == []
71
+ end
72
+
73
+ %w[/test http://example.com:8080/ /test?x=y /test?x=y&p=q].each do |key|
74
+ it "can read and write key: '#{key}'" do
75
+ lambda { @store.write(key, [[{},{}]]) }.should_not raise_error
76
+ @store.read(key).should == [[{},{}]]
77
+ end
78
+ end
79
+
80
+ it "can read and write fairly large keys" do
81
+ key = "b" * 4096
82
+ lambda { @store.write(key, [[{},{}]]) }.should_not raise_error
83
+ @store.read(key).should == [[{},{}]]
84
+ end
85
+
86
+ it "allows custom cache keys from block" do
87
+ request = mock_request('/test', {})
88
+ request.env['rack-cache.cache_key'] =
89
+ lambda { |request| request.path_info.reverse }
90
+ @store.cache_key(request).should == 'tset/'
91
+ end
92
+
93
+ it "allows custom cache keys from class" do
94
+ request = mock_request('/test', {})
95
+ request.env['rack-cache.cache_key'] = Class.new do
96
+ def self.call(request); request.path_info.reverse end
97
+ end
98
+ @store.cache_key(request).should == 'tset/'
99
+ end
100
+
101
+ # Abstract methods ===========================================================
102
+
103
+ # Stores an entry for the given request args, returns a url encoded cache key
104
+ # for the request.
105
+ define_method :store_simple_entry do |*request_args|
106
+ path, headers = request_args
107
+ @request = mock_request(path || '/test', headers || {})
108
+ @response = mock_response(200, {'Cache-Control' => 'max-age=420'}, ['test'])
109
+ body = @response.body
110
+ cache_key = @store.store(@request, @response, @entity_store)
111
+ @response.body.should_not equal(body)
112
+ cache_key
113
+ end
114
+
115
+ it 'stores a cache entry' do
116
+ cache_key = store_simple_entry
117
+ @store.read(cache_key).should_not be_empty
118
+ end
119
+
120
+ it 'sets the X-Content-Digest response header before storing' do
121
+ cache_key = store_simple_entry
122
+ req, res = @store.read(cache_key).first
123
+ res['X-Content-Digest'].should == 'a94a8fe5ccb19ba61c4c0873d391e987982fbbd3'
124
+ end
125
+
126
+ it 'finds a stored entry with #lookup' do
127
+ store_simple_entry
128
+ response = @store.lookup(@request, @entity_store)
129
+ response.should_not be_nil
130
+ response.should be_kind_of(Rack::Cache::Response)
131
+ end
132
+
133
+ it 'does not find an entry with #lookup when none exists' do
134
+ req = mock_request('/test', {'HTTP_FOO' => 'Foo', 'HTTP_BAR' => 'Bar'})
135
+ @store.lookup(req, @entity_store).should be_nil
136
+ end
137
+
138
+ it "canonizes urls for cache keys" do
139
+ store_simple_entry(path='/test?x=y&p=q')
140
+
141
+ hits_req = mock_request(path, {})
142
+ miss_req = mock_request('/test?p=x', {})
143
+
144
+ @store.lookup(hits_req, @entity_store).should_not be_nil
145
+ @store.lookup(miss_req, @entity_store).should be_nil
146
+ end
147
+
148
+ it 'does not find an entry with #lookup when the body does not exist' do
149
+ store_simple_entry
150
+ @response.headers['X-Content-Digest'].should_not be_nil
151
+ @entity_store.purge(@response.headers['X-Content-Digest'])
152
+ @store.lookup(@request, @entity_store).should be_nil
153
+ end
154
+
155
+ it 'restores response headers properly with #lookup' do
156
+ store_simple_entry
157
+ response = @store.lookup(@request, @entity_store)
158
+ response.headers.should == @response.headers.merge('Content-Length' => '4')
159
+ end
160
+
161
+ it 'restores response body from entity store with #lookup' do
162
+ store_simple_entry
163
+ response = @store.lookup(@request, @entity_store)
164
+ body = '' ; response.body.each {|p| body << p}
165
+ body.should == 'test'
166
+ end
167
+
168
+ it 'invalidates meta and entity store entries with #invalidate' do
169
+ store_simple_entry
170
+ @store.invalidate(@request, @entity_store)
171
+ response = @store.lookup(@request, @entity_store)
172
+ response.should be_kind_of(Rack::Cache::Response)
173
+ response.should_not be_fresh
174
+ end
175
+
176
+ it 'succeeds quietly when #invalidate called with no matching entries' do
177
+ req = mock_request('/test', {})
178
+ @store.invalidate(req, @entity_store)
179
+ @store.lookup(@request, @entity_store).should be_nil
180
+ end
181
+
182
+ # Vary =======================================================================
183
+
184
+ it 'does not return entries that Vary with #lookup' do
185
+ req1 = mock_request('/test', {'HTTP_FOO' => 'Foo', 'HTTP_BAR' => 'Bar'})
186
+ req2 = mock_request('/test', {'HTTP_FOO' => 'Bling', 'HTTP_BAR' => 'Bam'})
187
+ res = mock_response(200, {'Vary' => 'Foo Bar'}, ['test'])
188
+ @store.store(req1, res, @entity_store)
189
+
190
+ @store.lookup(req2, @entity_store).should be_nil
191
+ end
192
+
193
+ it 'stores multiple responses for each Vary combination' do
194
+ req1 = mock_request('/test', {'HTTP_FOO' => 'Foo', 'HTTP_BAR' => 'Bar'})
195
+ res1 = mock_response(200, {'Vary' => 'Foo Bar'}, ['test 1'])
196
+ key = @store.store(req1, res1, @entity_store)
197
+
198
+ req2 = mock_request('/test', {'HTTP_FOO' => 'Bling', 'HTTP_BAR' => 'Bam'})
199
+ res2 = mock_response(200, {'Vary' => 'Foo Bar'}, ['test 2'])
200
+ @store.store(req2, res2, @entity_store)
201
+
202
+ req3 = mock_request('/test', {'HTTP_FOO' => 'Baz', 'HTTP_BAR' => 'Boom'})
203
+ res3 = mock_response(200, {'Vary' => 'Foo Bar'}, ['test 3'])
204
+ @store.store(req3, res3, @entity_store)
205
+
206
+ slurp(@store.lookup(req3, @entity_store).body).should == 'test 3'
207
+ slurp(@store.lookup(req1, @entity_store).body).should == 'test 1'
208
+ slurp(@store.lookup(req2, @entity_store).body).should == 'test 2'
209
+
210
+ @store.read(key).length.should == 3
211
+ end
212
+
213
+ it 'overwrites non-varying responses with #store' do
214
+ req1 = mock_request('/test', {'HTTP_FOO' => 'Foo', 'HTTP_BAR' => 'Bar'})
215
+ res1 = mock_response(200, {'Vary' => 'Foo Bar'}, ['test 1'])
216
+ key = @store.store(req1, res1, @entity_store)
217
+ slurp(@store.lookup(req1, @entity_store).body).should == 'test 1'
218
+
219
+ req2 = mock_request('/test', {'HTTP_FOO' => 'Bling', 'HTTP_BAR' => 'Bam'})
220
+ res2 = mock_response(200, {'Vary' => 'Foo Bar'}, ['test 2'])
221
+ @store.store(req2, res2, @entity_store)
222
+ slurp(@store.lookup(req2, @entity_store).body).should == 'test 2'
223
+
224
+ req3 = mock_request('/test', {'HTTP_FOO' => 'Foo', 'HTTP_BAR' => 'Bar'})
225
+ res3 = mock_response(200, {'Vary' => 'Foo Bar'}, ['test 3'])
226
+ @store.store(req3, res3, @entity_store)
227
+ slurp(@store.lookup(req1, @entity_store).body).should == 'test 3'
228
+
229
+ @store.read(key).length.should == 2
230
+ end
231
+
232
+ # Helper Methods =============================================================
233
+
234
+ define_method :mock_request do |uri,opts|
235
+ env = Rack::MockRequest.env_for(uri, opts || {})
236
+ Rack::Cache::Request.new(env)
237
+ end
238
+
239
+ define_method :mock_response do |status,headers,body|
240
+ headers ||= {}
241
+ body = Array(body).compact
242
+ Rack::Cache::Response.new(status, headers, body)
243
+ end
244
+
245
+ define_method :slurp do |body|
246
+ buf = ''
247
+ body.each {|part| buf << part }
248
+ buf
249
+ end
250
+
251
+ define_method :uri do |uri|
252
+ URI.parse uri
253
+ end
254
+ end
255
+ end
256
+ end
257
+ end
@@ -0,0 +1,238 @@
1
+ require File.join(File.dirname(__FILE__), "/../../spec_helper")
2
+
3
+ module Rack
4
+ module Session
5
+ describe "Rack::Session::Redis" do
6
+ before(:each) do
7
+ @session_key = Rack::Session::Redis::DEFAULT_OPTIONS[:key]
8
+ @session_match = /#{@session_key}=[0-9a-fA-F]+;/
9
+ @incrementor = lambda do |env|
10
+ env["rack.session"]["counter"] ||= 0
11
+ env["rack.session"]["counter"] += 1
12
+ Rack::Response.new(env["rack.session"].inspect).to_a
13
+ end
14
+ @drop_session = proc do |env|
15
+ env['rack.session.options'][:drop] = true
16
+ @incrementor.call(env)
17
+ end
18
+ @renew_session = proc do |env|
19
+ env['rack.session.options'][:renew] = true
20
+ @incrementor.call(env)
21
+ end
22
+ @defer_session = proc do |env|
23
+ env['rack.session.options'][:defer] = true
24
+ @incrementor.call(env)
25
+ end
26
+ end
27
+
28
+ it "should specify connection params" do
29
+ pool = Rack::Session::Redis.new(@incrementor, :redis_server => "localhost:6380/1").pool
30
+ pool.should be_kind_of(MarshaledRedis)
31
+ pool.host.should == "localhost"
32
+ pool.port.should == 6380
33
+ pool.db.should == 1
34
+
35
+ pool = Rack::Session::Redis.new(@incrementor, :redis_server => ["localhost:6379", "localhost:6380"]).pool
36
+ pool.should be_kind_of(DistributedMarshaledRedis)
37
+ end
38
+
39
+ it "creates a new cookie" do
40
+ pool = Rack::Session::Redis.new(@incrementor)
41
+ res = Rack::MockRequest.new(pool).get("/")
42
+ res["Set-Cookie"].should match(/#{@session_key}=/)
43
+ res.body.should == '{"counter"=>1}'
44
+ end
45
+
46
+ it "determines session from a cookie" do
47
+ pool = Rack::Session::Redis.new(@incrementor)
48
+ req = Rack::MockRequest.new(pool)
49
+ res = req.get("/")
50
+ cookie = res["Set-Cookie"]
51
+ req.get("/", "HTTP_COOKIE" => cookie).
52
+ body.should == '{"counter"=>2}'
53
+ req.get("/", "HTTP_COOKIE" => cookie).
54
+ body.should == '{"counter"=>3}'
55
+ end
56
+
57
+ it "survives nonexistant cookies" do
58
+ bad_cookie = "rack.session=blarghfasel"
59
+ pool = Rack::Session::Redis.new(@incrementor)
60
+ res = Rack::MockRequest.new(pool).
61
+ get("/", "HTTP_COOKIE" => bad_cookie)
62
+ res.body.should == '{"counter"=>1}'
63
+ cookie = res["Set-Cookie"][@session_match]
64
+ cookie.should_not match(/#{bad_cookie}/)
65
+ end
66
+
67
+ it "should maintain freshness" do
68
+ pool = Rack::Session::Redis.new(@incrementor, :expire_after => 3)
69
+ res = Rack::MockRequest.new(pool).get('/')
70
+ res.body.should include('"counter"=>1')
71
+ cookie = res["Set-Cookie"]
72
+ res = Rack::MockRequest.new(pool).get('/', "HTTP_COOKIE" => cookie)
73
+ res["Set-Cookie"].should == cookie
74
+ res.body.should include('"counter"=>2')
75
+ puts 'Sleeping to expire session' if $DEBUG
76
+ sleep 4
77
+ res = Rack::MockRequest.new(pool).get('/', "HTTP_COOKIE" => cookie)
78
+ res["Set-Cookie"].should_not == cookie
79
+ res.body.should include('"counter"=>1')
80
+ end
81
+
82
+ it "deletes cookies with :drop option" do
83
+ pool = Rack::Session::Redis.new(@incrementor)
84
+ req = Rack::MockRequest.new(pool)
85
+ drop = Rack::Utils::Context.new(pool, @drop_session)
86
+ dreq = Rack::MockRequest.new(drop)
87
+
88
+ res0 = req.get("/")
89
+ session = (cookie = res0["Set-Cookie"])[@session_match]
90
+ res0.body.should == '{"counter"=>1}'
91
+
92
+ res1 = req.get("/", "HTTP_COOKIE" => cookie)
93
+ res1["Set-Cookie"][@session_match].should == session
94
+ res1.body.should == '{"counter"=>2}'
95
+
96
+ res2 = dreq.get("/", "HTTP_COOKIE" => cookie)
97
+ res2["Set-Cookie"].should be_nil
98
+ res2.body.should == '{"counter"=>3}'
99
+
100
+ res3 = req.get("/", "HTTP_COOKIE" => cookie)
101
+ res3["Set-Cookie"][@session_match].should_not == session
102
+ res3.body.should == '{"counter"=>1}'
103
+ end
104
+
105
+ it "provides new session id with :renew option" do
106
+ pool = Rack::Session::Redis.new(@incrementor)
107
+ req = Rack::MockRequest.new(pool)
108
+ renew = Rack::Utils::Context.new(pool, @renew_session)
109
+ rreq = Rack::MockRequest.new(renew)
110
+
111
+ res0 = req.get("/")
112
+ session = (cookie = res0["Set-Cookie"])[@session_match]
113
+ res0.body.should == '{"counter"=>1}'
114
+
115
+ res1 = req.get("/", "HTTP_COOKIE" => cookie)
116
+ res1["Set-Cookie"][@session_match].should == session
117
+ res1.body.should == '{"counter"=>2}'
118
+
119
+ res2 = rreq.get("/", "HTTP_COOKIE" => cookie)
120
+ new_cookie = res2["Set-Cookie"]
121
+ new_session = new_cookie[@session_match]
122
+ new_session.should_not == session
123
+ res2.body.should == '{"counter"=>3}'
124
+
125
+ res3 = req.get("/", "HTTP_COOKIE" => new_cookie)
126
+ res3["Set-Cookie"][@session_match].should == new_session
127
+ res3.body.should == '{"counter"=>4}'
128
+ end
129
+
130
+ specify "omits cookie with :defer option" do
131
+ pool = Rack::Session::Redis.new(@incrementor)
132
+ req = Rack::MockRequest.new(pool)
133
+ defer = Rack::Utils::Context.new(pool, @defer_session)
134
+ dreq = Rack::MockRequest.new(defer)
135
+
136
+ res0 = req.get("/")
137
+ session = (cookie = res0["Set-Cookie"])[@session_match]
138
+ res0.body.should == '{"counter"=>1}'
139
+
140
+ res1 = req.get("/", "HTTP_COOKIE" => cookie)
141
+ res1["Set-Cookie"][@session_match].should == session
142
+ res1.body.should == '{"counter"=>2}'
143
+
144
+ res2 = dreq.get("/", "HTTP_COOKIE" => cookie)
145
+ res2["Set-Cookie"].should be_nil
146
+ res2.body.should == '{"counter"=>3}'
147
+
148
+ res3 = req.get("/", "HTTP_COOKIE" => cookie)
149
+ res3["Set-Cookie"][@session_match].should == session
150
+ res3.body.should == '{"counter"=>4}'
151
+ end
152
+
153
+ # anyone know how to do this better?
154
+ specify "multithread: should cleanly merge sessions" do
155
+ next unless $DEBUG
156
+ warn 'Running multithread test for Session::Redis'
157
+ pool = Rack::Session::Redis.new(@incrementor)
158
+ req = Rack::MockRequest.new(pool)
159
+
160
+ res = req.get('/')
161
+ res.body.should == '{"counter"=>1}'
162
+ cookie = res["Set-Cookie"]
163
+ sess_id = cookie[/#{pool.key}=([^,;]+)/,1]
164
+
165
+ delta_incrementor = lambda do |env|
166
+ # emulate disconjoinment of threading
167
+ env['rack.session'] = env['rack.session'].dup
168
+ Thread.stop
169
+ env['rack.session'][(Time.now.usec*rand).to_i] = true
170
+ @incrementor.call(env)
171
+ end
172
+ tses = Rack::Utils::Context.new pool, delta_incrementor
173
+ treq = Rack::MockRequest.new(tses)
174
+ tnum = rand(7).to_i+5
175
+ r = Array.new(tnum) do
176
+ Thread.new(treq) do |run|
177
+ run.get('/', "HTTP_COOKIE" => cookie, 'rack.multithread' => true)
178
+ end
179
+ end.reverse.map{|t| t.run.join.value }
180
+ r.each do |res|
181
+ res['Set-Cookie'].should == cookie
182
+ res.body.should include('"counter"=>2')
183
+ end
184
+
185
+ session = pool.pool.get(sess_id)
186
+ session.size.should == tnum+1 # counter
187
+ session['counter'].should == 2 # meeeh
188
+
189
+ tnum = rand(7).to_i+5
190
+ r = Array.new(tnum) do |i|
191
+ delta_time = proc do |env|
192
+ env['rack.session'][i] = Time.now
193
+ Thread.stop
194
+ env['rack.session'] = env['rack.session'].dup
195
+ env['rack.session'][i] -= Time.now
196
+ @incrementor.call(env)
197
+ end
198
+ app = Rack::Utils::Context.new pool, time_delta
199
+ req = Rack::MockRequest.new app
200
+ Thread.new(req) do |run|
201
+ run.get('/', "HTTP_COOKIE" => cookie, 'rack.multithread' => true)
202
+ end
203
+ end.reverse.map{|t| t.run.join.value }
204
+ r.each do |res|
205
+ res['Set-Cookie'].should == cookie
206
+ res.body.should include('"counter"=>3')
207
+ end
208
+
209
+ session = pool.pool.get(sess_id)
210
+ session.size.should == tnum+1
211
+ session['counter'].should == 3
212
+
213
+ drop_counter = proc do |env|
214
+ env['rack.session'].delete 'counter'
215
+ env['rack.session']['foo'] = 'bar'
216
+ [200, {'Content-Type'=>'text/plain'}, env['rack.session'].inspect]
217
+ end
218
+ tses = Rack::Utils::Context.new pool, drop_counter
219
+ treq = Rack::MockRequest.new(tses)
220
+ tnum = rand(7).to_i+5
221
+ r = Array.new(tnum) do
222
+ Thread.new(treq) do |run|
223
+ run.get('/', "HTTP_COOKIE" => cookie, 'rack.multithread' => true)
224
+ end
225
+ end.reverse.map{|t| t.run.join.value }
226
+ r.each do |res|
227
+ res['Set-Cookie'].should == cookie
228
+ res.body.should include('"foo"=>"bar"')
229
+ end
230
+
231
+ session = pool.pool.get(sess_id)
232
+ session.size.should == r.size+1
233
+ session['counter'].should be_nil
234
+ session['foo'].should == 'bar'
235
+ end
236
+ end
237
+ end
238
+ end