xlymian-redis-store 0.3.8

Sign up to get free protection for your applications and to get access to all the features.
@@ -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.flush_all
17
+ @entity_store.cache.flush_all
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