nono-redis-store 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. data/.gitignore +11 -0
  2. data/CHANGELOG +262 -0
  3. data/Gemfile +33 -0
  4. data/Gemfile.lock +194 -0
  5. data/MIT-LICENSE +20 -0
  6. data/README.md +191 -0
  7. data/Rakefile +60 -0
  8. data/VERSION +1 -0
  9. data/lib/action_controller/session/redis_session_store.rb +74 -0
  10. data/lib/active_support/cache/redis_store.rb +228 -0
  11. data/lib/cache/merb/redis_store.rb +75 -0
  12. data/lib/cache/sinatra/redis_store.rb +126 -0
  13. data/lib/i18n/backend/redis.rb +67 -0
  14. data/lib/rack/cache/redis_entitystore.rb +51 -0
  15. data/lib/rack/cache/redis_metastore.rb +42 -0
  16. data/lib/rack/session/merb.rb +32 -0
  17. data/lib/rack/session/redis.rb +81 -0
  18. data/lib/redis-store.rb +44 -0
  19. data/lib/redis/distributed_store.rb +35 -0
  20. data/lib/redis/factory.rb +46 -0
  21. data/lib/redis/store.rb +30 -0
  22. data/lib/redis/store/interface.rb +17 -0
  23. data/lib/redis/store/marshalling.rb +41 -0
  24. data/lib/redis/store/namespace.rb +54 -0
  25. data/lib/redis/store/ttl.rb +37 -0
  26. data/lib/redis/store/version.rb +11 -0
  27. data/redis-store.gemspec +103 -0
  28. data/spec/action_controller/session/redis_session_store_spec.rb +121 -0
  29. data/spec/active_support/cache/redis_store_spec.rb +405 -0
  30. data/spec/cache/merb/redis_store_spec.rb +143 -0
  31. data/spec/cache/sinatra/redis_store_spec.rb +192 -0
  32. data/spec/config/master.conf +312 -0
  33. data/spec/config/single.conf +312 -0
  34. data/spec/config/slave.conf +312 -0
  35. data/spec/i18n/backend/redis_spec.rb +56 -0
  36. data/spec/rack/cache/entitystore/pony.jpg +0 -0
  37. data/spec/rack/cache/entitystore/redis_spec.rb +120 -0
  38. data/spec/rack/cache/metastore/redis_spec.rb +255 -0
  39. data/spec/rack/session/redis_spec.rb +234 -0
  40. data/spec/redis/distributed_store_spec.rb +47 -0
  41. data/spec/redis/factory_spec.rb +110 -0
  42. data/spec/redis/store/interface_spec.rb +23 -0
  43. data/spec/redis/store/marshalling_spec.rb +83 -0
  44. data/spec/redis/store/namespace_spec.rb +76 -0
  45. data/spec/redis/store/version_spec.rb +7 -0
  46. data/spec/spec_helper.rb +43 -0
  47. data/tasks/redis.tasks.rb +220 -0
  48. metadata +141 -0
@@ -0,0 +1,56 @@
1
+ require 'spec_helper'
2
+
3
+ describe "I18n::Backend::Redis" do
4
+ before :each do
5
+ @backend = I18n::Backend::Redis.new
6
+ @store = @backend.store
7
+ I18n.backend = @backend
8
+ end
9
+
10
+ it "should instantiate a store" do
11
+ @store.should be_kind_of(Redis::Store)
12
+ end
13
+
14
+ it "should instantiate a distributed store" do
15
+ store = I18n::Backend::Redis.new([ "redis://127.0.0.1:6379", "redis://127.0.0.1:6380" ]).store
16
+ store.should be_kind_of(Redis::DistributedStore)
17
+ end
18
+
19
+ it "should accept string uri" do
20
+ store = I18n::Backend::Redis.new("redis://127.0.0.1:6380/13/theplaylist").store
21
+ store.to_s.should == "Redis Client connected to 127.0.0.1:6380 against DB 13 with namespace theplaylist"
22
+ end
23
+
24
+ it "should accept hash params" do
25
+ store = I18n::Backend::Redis.new(:host => "127.0.0.1", :port => "6380", :db =>"13", :namespace => "theplaylist").store
26
+ store.to_s.should == "Redis Client connected to 127.0.0.1:6380 against DB 13 with namespace theplaylist"
27
+ end
28
+
29
+ it "should store translations" do
30
+ I18n.backend.store_translations :en, :foo => { :bar => :baz }
31
+ I18n.t(:"foo.bar").should == :baz
32
+
33
+ I18n.backend.store_translations :en, "foo" => { "bar" => "baz" }
34
+ I18n.t(:"foo.bar").should == "baz"
35
+ end
36
+
37
+ it "should get translations" do
38
+ I18n.backend.store_translations :en, :foo => { :bar => { :baz => :bang } }
39
+ I18n.t(:"foo.bar.baz").should == :bang
40
+ I18n.t(:"baz", :scope => :"foo.bar").should == :bang
41
+ end
42
+
43
+ it "should not store proc translations" do
44
+ lambda { I18n.backend.store_translations :en, :foo => lambda {|| } }.should raise_error("Key-value stores cannot handle procs")
45
+ end
46
+
47
+ it "should list available locales" do
48
+ locales = [ :en, :it, :es, :fr, :de ]
49
+ locales.each { |locale| I18n.backend.store_translations locale, :foo => "bar" }
50
+ available_locales = I18n.backend.available_locales
51
+
52
+ locales.each do |locale|
53
+ available_locales.should include(locale)
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,120 @@
1
+ require 'spec_helper'
2
+
3
+ class Object
4
+ def sha_like?
5
+ length == 40 && self =~ /^[0-9a-z]+$/
6
+ end
7
+ end
8
+
9
+ module Rack
10
+ module Cache
11
+ class EntityStore
12
+ # courtesy of http://github.com/rtomayko/rack-cache team
13
+ describe "Rack::Cache::EntityStore::Redis" do
14
+ before(:each) do
15
+ @store = Rack::Cache::EntityStore::Redis.new :host => "localhost"
16
+ end
17
+
18
+ # Redis store specific examples ===========================================
19
+
20
+ it "should have the class referenced by homonym constant" do
21
+ Rack::Cache::EntityStore::REDIS.should be(Rack::Cache::EntityStore::Redis)
22
+ end
23
+
24
+ it "should resolve the connection uri" do
25
+ cache = Rack::Cache::EntityStore::Redis.resolve(uri("redis://127.0.0.1")).cache
26
+ cache.should be_kind_of(::Redis)
27
+ cache.id.should == "redis://127.0.0.1:6379/0"
28
+
29
+ cache = Rack::Cache::EntityStore::Redis.resolve(uri("redis://127.0.0.1:6380")).cache
30
+ cache.id.should == "redis://127.0.0.1:6380/0"
31
+
32
+ cache = Rack::Cache::EntityStore::Redis.resolve(uri("redis://127.0.0.1/13")).cache
33
+ cache.id.should == "redis://127.0.0.1:6379/13"
34
+ end
35
+
36
+ # Entity store shared examples ===========================================
37
+
38
+ it 'responds to all required messages' do
39
+ %w[read open write exist?].each do |message|
40
+ @store.should respond_to(message)
41
+ end
42
+ end
43
+
44
+ it 'stores bodies with #write' do
45
+ key, size = @store.write(['My wild love went riding,'])
46
+ key.should_not be_nil
47
+ key.should be_sha_like
48
+
49
+ data = @store.read(key)
50
+ data.should == 'My wild love went riding,'
51
+ end
52
+
53
+ it 'correctly determines whether cached body exists for key with #exist?' do
54
+ key, size = @store.write(['She rode to the devil,'])
55
+ @store.should be_exist(key)
56
+ @store.should_not be_exist('938jasddj83jasdh4438021ksdfjsdfjsdsf')
57
+ end
58
+
59
+ it 'can read data written with #write' do
60
+ key, size = @store.write(['And asked him to pay.'])
61
+ data = @store.read(key)
62
+ data.should == 'And asked him to pay.'
63
+ end
64
+
65
+ it 'gives a 40 character SHA1 hex digest from #write' do
66
+ key, size = @store.write(['she rode to the sea;'])
67
+ key.should_not be_nil
68
+ key.length.should == 40
69
+ key.should =~ /^[0-9a-z]+$/
70
+ key.should == '90a4c84d51a277f3dafc34693ca264531b9f51b6'
71
+ end
72
+
73
+ it 'returns the entire body as a String from #read' do
74
+ key, size = @store.write(['She gathered together'])
75
+ @store.read(key).should == 'She gathered together'
76
+ end
77
+
78
+ it 'returns nil from #read when key does not exist' do
79
+ @store.read('87fe0a1ae82a518592f6b12b0183e950b4541c62').should be_nil
80
+ end
81
+
82
+ it 'returns a Rack compatible body from #open' do
83
+ key, size = @store.write(['Some shells for her hair.'])
84
+ body = @store.open(key)
85
+ body.should respond_to(:each)
86
+ buf = ''
87
+ body.each { |part| buf << part }
88
+ buf.should == 'Some shells for her hair.'
89
+ end
90
+
91
+ it 'returns nil from #open when key does not exist' do
92
+ @store.open('87fe0a1ae82a518592f6b12b0183e950b4541c62').should be_nil
93
+ end
94
+
95
+ if RUBY_VERSION < '1.9'
96
+ it 'can store largish bodies with binary data' do
97
+ pony = ::File.open(::File.dirname(__FILE__) + '/pony.jpg', 'rb') { |f| f.read }
98
+ key, size = @store.write([pony])
99
+ key.should == 'd0f30d8659b4d268c5c64385d9790024c2d78deb'
100
+ data = @store.read(key)
101
+ data.length.should == pony.length
102
+ data.hash.should == pony.hash
103
+ end
104
+ end
105
+
106
+ it 'deletes stored entries with #purge' do
107
+ key, size = @store.write(['My wild love went riding,'])
108
+ @store.purge(key).should be_nil
109
+ @store.read(key).should be_nil
110
+ end
111
+
112
+ # Helper Methods =============================================================
113
+
114
+ define_method :uri do |uri|
115
+ URI.parse uri
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,255 @@
1
+ require '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(::Redis::Store)
27
+ cache.to_s.should == "Redis Client connected to 127.0.0.1:6379 against DB 0"
28
+
29
+ cache = Rack::Cache::MetaStore::Redis.resolve(uri("redis://127.0.0.1:6380")).cache
30
+ cache.to_s.should == "Redis Client connected to 127.0.0.1:6380 against DB 0"
31
+
32
+ cache = Rack::Cache::MetaStore::Redis.resolve(uri("redis://127.0.0.1/13")).cache
33
+ cache.to_s.should == "Redis Client connected to 127.0.0.1:6379 against DB 13"
34
+ end
35
+
36
+ # Low-level implementation methods ===========================================
37
+
38
+ it 'writes a list of negotation tuples with #write' do
39
+ lambda { @store.write('/test', [[{}, {}]]) }.should_not raise_error
40
+ end
41
+
42
+ it 'reads a list of negotation tuples with #read' do
43
+ @store.write('/test', [[{},{}],[{},{}]])
44
+ tuples = @store.read('/test')
45
+ tuples.should == [ [{},{}], [{},{}] ]
46
+ end
47
+
48
+ it 'reads an empty list with #read when nothing cached at key' do
49
+ @store.read('/nothing').should be_empty
50
+ end
51
+
52
+ it 'removes entries for key with #purge' do
53
+ @store.write('/test', [[{},{}]])
54
+ @store.read('/test').should_not be_empty
55
+
56
+ @store.purge('/test')
57
+ @store.read('/test').should be_empty
58
+ end
59
+
60
+ it 'succeeds when purging non-existing entries' do
61
+ @store.read('/test').should be_empty
62
+ @store.purge('/test')
63
+ end
64
+
65
+ it 'returns nil from #purge' do
66
+ @store.write('/test', [[{},{}]])
67
+ @store.purge('/test').should be_nil
68
+ @store.read('/test').should == []
69
+ end
70
+
71
+ %w[/test http://example.com:8080/ /test?x=y /test?x=y&p=q].each do |key|
72
+ it "can read and write key: '#{key}'" do
73
+ lambda { @store.write(key, [[{},{}]]) }.should_not raise_error
74
+ @store.read(key).should == [[{},{}]]
75
+ end
76
+ end
77
+
78
+ it "can read and write fairly large keys" do
79
+ key = "b" * 4096
80
+ lambda { @store.write(key, [[{},{}]]) }.should_not raise_error
81
+ @store.read(key).should == [[{},{}]]
82
+ end
83
+
84
+ it "allows custom cache keys from block" do
85
+ request = mock_request('/test', {})
86
+ request.env['rack-cache.cache_key'] =
87
+ lambda { |request| request.path_info.reverse }
88
+ @store.cache_key(request).should == 'tset/'
89
+ end
90
+
91
+ it "allows custom cache keys from class" do
92
+ request = mock_request('/test', {})
93
+ request.env['rack-cache.cache_key'] = Class.new do
94
+ def self.call(request); request.path_info.reverse end
95
+ end
96
+ @store.cache_key(request).should == 'tset/'
97
+ end
98
+
99
+ # Abstract methods ===========================================================
100
+
101
+ # Stores an entry for the given request args, returns a url encoded cache key
102
+ # for the request.
103
+ define_method :store_simple_entry do |*request_args|
104
+ path, headers = request_args
105
+ @request = mock_request(path || '/test', headers || {})
106
+ @response = mock_response(200, {'Cache-Control' => 'max-age=420'}, ['test'])
107
+ body = @response.body
108
+ cache_key = @store.store(@request, @response, @entity_store)
109
+ @response.body.should_not equal(body)
110
+ cache_key
111
+ end
112
+
113
+ it 'stores a cache entry' do
114
+ cache_key = store_simple_entry
115
+ @store.read(cache_key).should_not be_empty
116
+ end
117
+
118
+ it 'sets the X-Content-Digest response header before storing' do
119
+ cache_key = store_simple_entry
120
+ req, res = @store.read(cache_key).first
121
+ res['X-Content-Digest'].should == 'a94a8fe5ccb19ba61c4c0873d391e987982fbbd3'
122
+ end
123
+
124
+ it 'finds a stored entry with #lookup' do
125
+ store_simple_entry
126
+ response = @store.lookup(@request, @entity_store)
127
+ response.should_not be_nil
128
+ response.should be_kind_of(Rack::Cache::Response)
129
+ end
130
+
131
+ it 'does not find an entry with #lookup when none exists' do
132
+ req = mock_request('/test', {'HTTP_FOO' => 'Foo', 'HTTP_BAR' => 'Bar'})
133
+ @store.lookup(req, @entity_store).should be_nil
134
+ end
135
+
136
+ it "canonizes urls for cache keys" do
137
+ store_simple_entry(path='/test?x=y&p=q')
138
+
139
+ hits_req = mock_request(path, {})
140
+ miss_req = mock_request('/test?p=x', {})
141
+
142
+ @store.lookup(hits_req, @entity_store).should_not be_nil
143
+ @store.lookup(miss_req, @entity_store).should be_nil
144
+ end
145
+
146
+ it 'does not find an entry with #lookup when the body does not exist' do
147
+ store_simple_entry
148
+ @response.headers['X-Content-Digest'].should_not be_nil
149
+ @entity_store.purge(@response.headers['X-Content-Digest'])
150
+ @store.lookup(@request, @entity_store).should be_nil
151
+ end
152
+
153
+ it 'restores response headers properly with #lookup' do
154
+ store_simple_entry
155
+ response = @store.lookup(@request, @entity_store)
156
+ response.headers.should == @response.headers.merge('Content-Length' => '4')
157
+ end
158
+
159
+ it 'restores response body from entity store with #lookup' do
160
+ store_simple_entry
161
+ response = @store.lookup(@request, @entity_store)
162
+ body = '' ; response.body.each {|p| body << p}
163
+ body.should == 'test'
164
+ end
165
+
166
+ it 'invalidates meta and entity store entries with #invalidate' do
167
+ store_simple_entry
168
+ @store.invalidate(@request, @entity_store)
169
+ response = @store.lookup(@request, @entity_store)
170
+ response.should be_kind_of(Rack::Cache::Response)
171
+ response.should_not be_fresh
172
+ end
173
+
174
+ it 'succeeds quietly when #invalidate called with no matching entries' do
175
+ req = mock_request('/test', {})
176
+ @store.invalidate(req, @entity_store)
177
+ @store.lookup(@request, @entity_store).should be_nil
178
+ end
179
+
180
+ # Vary =======================================================================
181
+
182
+ it 'does not return entries that Vary with #lookup' do
183
+ req1 = mock_request('/test', {'HTTP_FOO' => 'Foo', 'HTTP_BAR' => 'Bar'})
184
+ req2 = mock_request('/test', {'HTTP_FOO' => 'Bling', 'HTTP_BAR' => 'Bam'})
185
+ res = mock_response(200, {'Vary' => 'Foo Bar'}, ['test'])
186
+ @store.store(req1, res, @entity_store)
187
+
188
+ @store.lookup(req2, @entity_store).should be_nil
189
+ end
190
+
191
+ it 'stores multiple responses for each Vary combination' 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
+
196
+ req2 = mock_request('/test', {'HTTP_FOO' => 'Bling', 'HTTP_BAR' => 'Bam'})
197
+ res2 = mock_response(200, {'Vary' => 'Foo Bar'}, ['test 2'])
198
+ @store.store(req2, res2, @entity_store)
199
+
200
+ req3 = mock_request('/test', {'HTTP_FOO' => 'Baz', 'HTTP_BAR' => 'Boom'})
201
+ res3 = mock_response(200, {'Vary' => 'Foo Bar'}, ['test 3'])
202
+ @store.store(req3, res3, @entity_store)
203
+
204
+ slurp(@store.lookup(req3, @entity_store).body).should == 'test 3'
205
+ slurp(@store.lookup(req1, @entity_store).body).should == 'test 1'
206
+ slurp(@store.lookup(req2, @entity_store).body).should == 'test 2'
207
+
208
+ @store.read(key).length.should == 3
209
+ end
210
+
211
+ it 'overwrites non-varying responses with #store' do
212
+ req1 = mock_request('/test', {'HTTP_FOO' => 'Foo', 'HTTP_BAR' => 'Bar'})
213
+ res1 = mock_response(200, {'Vary' => 'Foo Bar'}, ['test 1'])
214
+ key = @store.store(req1, res1, @entity_store)
215
+ slurp(@store.lookup(req1, @entity_store).body).should == 'test 1'
216
+
217
+ req2 = mock_request('/test', {'HTTP_FOO' => 'Bling', 'HTTP_BAR' => 'Bam'})
218
+ res2 = mock_response(200, {'Vary' => 'Foo Bar'}, ['test 2'])
219
+ @store.store(req2, res2, @entity_store)
220
+ slurp(@store.lookup(req2, @entity_store).body).should == 'test 2'
221
+
222
+ req3 = mock_request('/test', {'HTTP_FOO' => 'Foo', 'HTTP_BAR' => 'Bar'})
223
+ res3 = mock_response(200, {'Vary' => 'Foo Bar'}, ['test 3'])
224
+ @store.store(req3, res3, @entity_store)
225
+ slurp(@store.lookup(req1, @entity_store).body).should == 'test 3'
226
+
227
+ @store.read(key).length.should == 2
228
+ end
229
+
230
+ # Helper Methods =============================================================
231
+
232
+ define_method :mock_request do |uri,opts|
233
+ env = Rack::MockRequest.env_for(uri, opts || {})
234
+ Rack::Cache::Request.new(env)
235
+ end
236
+
237
+ define_method :mock_response do |status,headers,body|
238
+ headers ||= {}
239
+ body = Array(body).compact
240
+ Rack::Cache::Response.new(status, headers, body)
241
+ end
242
+
243
+ define_method :slurp do |body|
244
+ buf = ''
245
+ body.each {|part| buf << part }
246
+ buf
247
+ end
248
+
249
+ define_method :uri do |uri|
250
+ URI.parse uri
251
+ end
252
+ end
253
+ end
254
+ end
255
+ end
@@ -0,0 +1,234 @@
1
+ require '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 => "redis://127.0.0.1:6380/1/theplaylist").pool
30
+ pool.should be_kind_of(::Redis::Store)
31
+ pool.to_s.should == "Redis Client connected to 127.0.0.1:6380 against DB 1 with namespace theplaylist"
32
+
33
+ pool = Rack::Session::Redis.new(@incrementor, :redis_server => ["redis://127.0.0.1:6379", "redis://127.0.0.1:6380"]).pool
34
+ pool.should be_kind_of(::Redis::DistributedStore)
35
+ end
36
+
37
+ it "creates a new cookie" do
38
+ pool = Rack::Session::Redis.new(@incrementor)
39
+ res = Rack::MockRequest.new(pool).get("/")
40
+ res["Set-Cookie"].should match(/#{@session_key}=/)
41
+ res.body.should == '{"counter"=>1}'
42
+ end
43
+
44
+ it "determines session from a cookie" do
45
+ pool = Rack::Session::Redis.new(@incrementor)
46
+ req = Rack::MockRequest.new(pool)
47
+ res = req.get("/")
48
+ cookie = res["Set-Cookie"]
49
+ req.get("/", "HTTP_COOKIE" => cookie).body.should == '{"counter"=>2}'
50
+ req.get("/", "HTTP_COOKIE" => cookie).body.should == '{"counter"=>3}'
51
+ end
52
+
53
+ it "survives nonexistant cookies" do
54
+ bad_cookie = "rack.session=blarghfasel"
55
+ pool = Rack::Session::Redis.new(@incrementor)
56
+ res = Rack::MockRequest.new(pool).
57
+ get("/", "HTTP_COOKIE" => bad_cookie)
58
+ res.body.should == '{"counter"=>1}'
59
+ cookie = res["Set-Cookie"][@session_match]
60
+ cookie.should_not match(/#{bad_cookie}/)
61
+ end
62
+
63
+ it "should maintain freshness" do
64
+ pool = Rack::Session::Redis.new(@incrementor, :expire_after => 3)
65
+ res = Rack::MockRequest.new(pool).get('/')
66
+ res.body.should include('"counter"=>1')
67
+ cookie = res["Set-Cookie"]
68
+ res = Rack::MockRequest.new(pool).get('/', "HTTP_COOKIE" => cookie)
69
+ res["Set-Cookie"].should == cookie
70
+ res.body.should include('"counter"=>2')
71
+ puts 'Sleeping to expire session' if $DEBUG
72
+ sleep 4
73
+ res = Rack::MockRequest.new(pool).get('/', "HTTP_COOKIE" => cookie)
74
+ res["Set-Cookie"].should_not == cookie
75
+ res.body.should include('"counter"=>1')
76
+ end
77
+
78
+ it "deletes cookies with :drop option" do
79
+ pool = Rack::Session::Redis.new(@incrementor)
80
+ req = Rack::MockRequest.new(pool)
81
+ drop = Rack::Utils::Context.new(pool, @drop_session)
82
+ dreq = Rack::MockRequest.new(drop)
83
+
84
+ res0 = req.get("/")
85
+ session = (cookie = res0["Set-Cookie"])[@session_match]
86
+ res0.body.should == '{"counter"=>1}'
87
+
88
+ res1 = req.get("/", "HTTP_COOKIE" => cookie)
89
+ res1["Set-Cookie"][@session_match].should == session
90
+ res1.body.should == '{"counter"=>2}'
91
+
92
+ res2 = dreq.get("/", "HTTP_COOKIE" => cookie)
93
+ res2["Set-Cookie"].should be_nil
94
+ res2.body.should == '{"counter"=>3}'
95
+
96
+ res3 = req.get("/", "HTTP_COOKIE" => cookie)
97
+ res3["Set-Cookie"][@session_match].should_not == session
98
+ res3.body.should == '{"counter"=>1}'
99
+ end
100
+
101
+ it "provides new session id with :renew option" do
102
+ pool = Rack::Session::Redis.new(@incrementor)
103
+ req = Rack::MockRequest.new(pool)
104
+ renew = Rack::Utils::Context.new(pool, @renew_session)
105
+ rreq = Rack::MockRequest.new(renew)
106
+
107
+ res0 = req.get("/")
108
+ session = (cookie = res0["Set-Cookie"])[@session_match]
109
+ res0.body.should == '{"counter"=>1}'
110
+
111
+ res1 = req.get("/", "HTTP_COOKIE" => cookie)
112
+ res1["Set-Cookie"][@session_match].should == session
113
+ res1.body.should == '{"counter"=>2}'
114
+
115
+ res2 = rreq.get("/", "HTTP_COOKIE" => cookie)
116
+ new_cookie = res2["Set-Cookie"]
117
+ new_session = new_cookie[@session_match]
118
+ new_session.should_not == session
119
+ res2.body.should == '{"counter"=>3}'
120
+
121
+ res3 = req.get("/", "HTTP_COOKIE" => new_cookie)
122
+ res3["Set-Cookie"][@session_match].should == new_session
123
+ res3.body.should == '{"counter"=>4}'
124
+ end
125
+
126
+ specify "omits cookie with :defer option" do
127
+ pool = Rack::Session::Redis.new(@incrementor)
128
+ req = Rack::MockRequest.new(pool)
129
+ defer = Rack::Utils::Context.new(pool, @defer_session)
130
+ dreq = Rack::MockRequest.new(defer)
131
+
132
+ res0 = req.get("/")
133
+ session = (cookie = res0["Set-Cookie"])[@session_match]
134
+ res0.body.should == '{"counter"=>1}'
135
+
136
+ res1 = req.get("/", "HTTP_COOKIE" => cookie)
137
+ res1["Set-Cookie"][@session_match].should == session
138
+ res1.body.should == '{"counter"=>2}'
139
+
140
+ res2 = dreq.get("/", "HTTP_COOKIE" => cookie)
141
+ res2["Set-Cookie"].should be_nil
142
+ res2.body.should == '{"counter"=>3}'
143
+
144
+ res3 = req.get("/", "HTTP_COOKIE" => cookie)
145
+ res3["Set-Cookie"][@session_match].should == session
146
+ res3.body.should == '{"counter"=>4}'
147
+ end
148
+
149
+ # anyone know how to do this better?
150
+ specify "multithread: should cleanly merge sessions" do
151
+ next unless $DEBUG
152
+ warn 'Running multithread test for Session::Redis'
153
+ pool = Rack::Session::Redis.new(@incrementor)
154
+ req = Rack::MockRequest.new(pool)
155
+
156
+ res = req.get('/')
157
+ res.body.should == '{"counter"=>1}'
158
+ cookie = res["Set-Cookie"]
159
+ sess_id = cookie[/#{pool.key}=([^,;]+)/,1]
160
+
161
+ delta_incrementor = lambda do |env|
162
+ # emulate disconjoinment of threading
163
+ env['rack.session'] = env['rack.session'].dup
164
+ Thread.stop
165
+ env['rack.session'][(Time.now.usec*rand).to_i] = true
166
+ @incrementor.call(env)
167
+ end
168
+ tses = Rack::Utils::Context.new pool, delta_incrementor
169
+ treq = Rack::MockRequest.new(tses)
170
+ tnum = rand(7).to_i+5
171
+ r = Array.new(tnum) do
172
+ Thread.new(treq) do |run|
173
+ run.get('/', "HTTP_COOKIE" => cookie, 'rack.multithread' => true)
174
+ end
175
+ end.reverse.map{|t| t.run.join.value }
176
+ r.each do |res|
177
+ res['Set-Cookie'].should == cookie
178
+ res.body.should include('"counter"=>2')
179
+ end
180
+
181
+ session = pool.pool.get(sess_id)
182
+ session.size.should == tnum+1 # counter
183
+ session['counter'].should == 2 # meeeh
184
+
185
+ tnum = rand(7).to_i+5
186
+ r = Array.new(tnum) do |i|
187
+ delta_time = proc do |env|
188
+ env['rack.session'][i] = Time.now
189
+ Thread.stop
190
+ env['rack.session'] = env['rack.session'].dup
191
+ env['rack.session'][i] -= Time.now
192
+ @incrementor.call(env)
193
+ end
194
+ app = Rack::Utils::Context.new pool, time_delta
195
+ req = Rack::MockRequest.new app
196
+ Thread.new(req) do |run|
197
+ run.get('/', "HTTP_COOKIE" => cookie, 'rack.multithread' => true)
198
+ end
199
+ end.reverse.map{|t| t.run.join.value }
200
+ r.each do |res|
201
+ res['Set-Cookie'].should == cookie
202
+ res.body.should include('"counter"=>3')
203
+ end
204
+
205
+ session = pool.pool.get(sess_id)
206
+ session.size.should == tnum+1
207
+ session['counter'].should == 3
208
+
209
+ drop_counter = proc do |env|
210
+ env['rack.session'].del 'counter'
211
+ env['rack.session']['foo'] = 'bar'
212
+ [200, {'Content-Type'=>'text/plain'}, env['rack.session'].inspect]
213
+ end
214
+ tses = Rack::Utils::Context.new pool, drop_counter
215
+ treq = Rack::MockRequest.new(tses)
216
+ tnum = rand(7).to_i+5
217
+ r = Array.new(tnum) do
218
+ Thread.new(treq) do |run|
219
+ run.get('/', "HTTP_COOKIE" => cookie, 'rack.multithread' => true)
220
+ end
221
+ end.reverse.map{|t| t.run.join.value }
222
+ r.each do |res|
223
+ res['Set-Cookie'].should == cookie
224
+ res.body.should include('"foo"=>"bar"')
225
+ end
226
+
227
+ session = pool.pool.get(sess_id)
228
+ session.size.should == r.size+1
229
+ session['counter'].should be_nil
230
+ session['foo'].should == 'bar'
231
+ end
232
+ end
233
+ end
234
+ end