redis-client-session-store 0.12
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.
- checksums.yaml +7 -0
- data/.github/workflows/ruby.yml +42 -0
- data/.gitignore +16 -0
- data/.gitmodules +3 -0
- data/.rspec +3 -0
- data/.rubocop.yml +43 -0
- data/.rubocop_todo.yml +67 -0
- data/.simplecov +5 -0
- data/AUTHORS.md +29 -0
- data/CODE_OF_CONDUCT.md +75 -0
- data/CONTRIBUTING.md +6 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +144 -0
- data/LICENSE +22 -0
- data/README.md +99 -0
- data/Rakefile +15 -0
- data/lib/redis-client-session-store.rb +225 -0
- data/redis-client-session-store.gemspec +26 -0
- data/spec/redis_client_session_store_spec.rb +646 -0
- data/spec/spec_helper.rb +32 -0
- data/spec/support/redis_double.rb +9 -0
- data/spec/support.rb +86 -0
- metadata +186 -0
@@ -0,0 +1,646 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
describe RedisSessionStore do
|
4
|
+
subject(:store) { described_class.new(nil, redis_options.merge(options)) }
|
5
|
+
|
6
|
+
let :random_string do
|
7
|
+
"#{rand}#{rand}#{rand}"
|
8
|
+
end
|
9
|
+
let :default_options do
|
10
|
+
store.instance_variable_get(:@default_options)
|
11
|
+
end
|
12
|
+
|
13
|
+
let :options do
|
14
|
+
{}
|
15
|
+
end
|
16
|
+
|
17
|
+
let :redis_options do
|
18
|
+
{ redis: { url: ENV['REDIS_URL'] } }
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'assigns a :namespace to @default_options' do
|
22
|
+
expect(default_options[:namespace]).to eq('rack:session')
|
23
|
+
end
|
24
|
+
|
25
|
+
describe 'when initializing with the redis sub-hash options' do
|
26
|
+
let :options do
|
27
|
+
{
|
28
|
+
key: random_string,
|
29
|
+
secret: random_string,
|
30
|
+
redis: {
|
31
|
+
host: 'hosty.local',
|
32
|
+
port: 16_379,
|
33
|
+
db: 2,
|
34
|
+
key_prefix: 'myapp:session:',
|
35
|
+
expire_after: 60 * 120
|
36
|
+
}
|
37
|
+
}
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'creates a redis instance' do
|
41
|
+
expect(store.instance_variable_get(:@redis)).not_to be_nil
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'assigns the :host option to @default_options' do
|
45
|
+
expect(default_options[:host]).to eq('hosty.local')
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'assigns the :port option to @default_options' do
|
49
|
+
expect(default_options[:port]).to eq(16_379)
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'assigns the :db option to @default_options' do
|
53
|
+
expect(default_options[:db]).to eq(2)
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'assigns the :key_prefix option to @default_options' do
|
57
|
+
expect(default_options[:key_prefix]).to eq('myapp:session:')
|
58
|
+
end
|
59
|
+
|
60
|
+
it 'assigns the :expire_after option to @default_options' do
|
61
|
+
expect(default_options[:expire_after]).to eq(60 * 120)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
describe 'when configured with both :ttl and :expire_after' do
|
66
|
+
let(:ttl_seconds) { 60 * 120 }
|
67
|
+
let :options do
|
68
|
+
{
|
69
|
+
key: random_string,
|
70
|
+
secret: random_string,
|
71
|
+
redis: {
|
72
|
+
host: 'hosty.local',
|
73
|
+
port: 16_379,
|
74
|
+
db: 2,
|
75
|
+
key_prefix: 'myapp:session:',
|
76
|
+
ttl: ttl_seconds,
|
77
|
+
expire_after: nil
|
78
|
+
}
|
79
|
+
}
|
80
|
+
end
|
81
|
+
|
82
|
+
it 'assigns the :ttl option to @default_options' do
|
83
|
+
expect(default_options[:ttl]).to eq(ttl_seconds)
|
84
|
+
expect(default_options[:expire_after]).to be_nil
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
describe 'when initializing with top-level redis options' do
|
89
|
+
let :options do
|
90
|
+
{
|
91
|
+
key: random_string,
|
92
|
+
secret: random_string,
|
93
|
+
host: 'hostersons.local',
|
94
|
+
port: 26_379,
|
95
|
+
db: 4,
|
96
|
+
key_prefix: 'appydoo:session:',
|
97
|
+
expire_after: 60 * 60
|
98
|
+
}
|
99
|
+
end
|
100
|
+
|
101
|
+
it 'creates a redis instance' do
|
102
|
+
expect(store.instance_variable_get(:@redis)).not_to be_nil
|
103
|
+
end
|
104
|
+
|
105
|
+
it 'assigns the :host option to @default_options' do
|
106
|
+
expect(default_options[:host]).to eq('hostersons.local')
|
107
|
+
end
|
108
|
+
|
109
|
+
it 'assigns the :port option to @default_options' do
|
110
|
+
expect(default_options[:port]).to eq(26_379)
|
111
|
+
end
|
112
|
+
|
113
|
+
it 'assigns the :db option to @default_options' do
|
114
|
+
expect(default_options[:db]).to eq(4)
|
115
|
+
end
|
116
|
+
|
117
|
+
it 'assigns the :key_prefix option to @default_options' do
|
118
|
+
expect(default_options[:key_prefix]).to eq('appydoo:session:')
|
119
|
+
end
|
120
|
+
|
121
|
+
it 'assigns the :expire_after option to @default_options' do
|
122
|
+
expect(default_options[:expire_after]).to eq(60 * 60)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
describe 'when initializing with existing redis object' do
|
127
|
+
let :options do
|
128
|
+
{
|
129
|
+
key: random_string,
|
130
|
+
secret: random_string,
|
131
|
+
redis: {
|
132
|
+
client: redis_client,
|
133
|
+
key_prefix: 'myapp:session:',
|
134
|
+
expire_after: 60 * 30
|
135
|
+
}
|
136
|
+
}
|
137
|
+
end
|
138
|
+
|
139
|
+
let(:redis_client) { double('redis_client') }
|
140
|
+
|
141
|
+
it 'assigns given redis object to @redis' do
|
142
|
+
expect(store.instance_variable_get(:@redis)).to be(redis_client)
|
143
|
+
end
|
144
|
+
|
145
|
+
it 'assigns the :client option to @default_options' do
|
146
|
+
expect(default_options[:client]).to be(redis_client)
|
147
|
+
end
|
148
|
+
|
149
|
+
it 'assigns the :key_prefix option to @default_options' do
|
150
|
+
expect(default_options[:key_prefix]).to eq('myapp:session:')
|
151
|
+
end
|
152
|
+
|
153
|
+
it 'assigns the :expire_after option to @default_options' do
|
154
|
+
expect(default_options[:expire_after]).to eq(60 * 30)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
describe 'rack 1.45 compatibility' do
|
159
|
+
# Rack 1.45 (which Rails 3.2.x depends on) uses the return value of
|
160
|
+
# set_session to set the cookie value. See:
|
161
|
+
# https://github.com/rack/rack/blob/1.4.5/lib/rack/session/abstract/id.rb
|
162
|
+
|
163
|
+
let(:env) { double('env') }
|
164
|
+
let(:session_id) { Rack::Session::SessionId.new('12 345') }
|
165
|
+
let(:session_data) { double('session_data') }
|
166
|
+
let(:options) { { expire_after: 123 } }
|
167
|
+
|
168
|
+
context 'when successfully persisting the session' do
|
169
|
+
it 'returns the session id' do
|
170
|
+
expect(store.send(:set_session, env, session_id, session_data, options))
|
171
|
+
.to eq(session_id)
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
context 'when unsuccessfully persisting the session' do
|
176
|
+
before do
|
177
|
+
allow(store).to receive(:redis).and_raise(RedisClient::CannotConnectError)
|
178
|
+
end
|
179
|
+
|
180
|
+
it 'returns false' do
|
181
|
+
expect(store.send(:set_session, env, session_id, session_data, options))
|
182
|
+
.to eq(false)
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
context 'when no expire_after option is given' do
|
187
|
+
let(:options) { {} }
|
188
|
+
|
189
|
+
it 'sets the session value without expiry' do
|
190
|
+
expect(store.send(:set_session, env, session_id, session_data, options))
|
191
|
+
.to eq(session_id)
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
context 'when redis is down' do
|
196
|
+
before do
|
197
|
+
allow(store).to receive(:redis).and_raise(RedisClient::CannotConnectError)
|
198
|
+
store.on_redis_down = ->(*_a) { @redis_down_handled = true }
|
199
|
+
end
|
200
|
+
|
201
|
+
it 'returns false' do
|
202
|
+
expect(store.send(:set_session, env, session_id, session_data, options))
|
203
|
+
.to eq(false)
|
204
|
+
end
|
205
|
+
|
206
|
+
it 'calls the on_redis_down handler' do
|
207
|
+
store.send(:set_session, env, session_id, session_data, options)
|
208
|
+
expect(@redis_down_handled).to eq(true)
|
209
|
+
end
|
210
|
+
|
211
|
+
context 'when :on_redis_down re-raises' do
|
212
|
+
before { store.on_redis_down = ->(e, *) { raise e } }
|
213
|
+
|
214
|
+
it 'explodes' do
|
215
|
+
expect do
|
216
|
+
store.send(:set_session, env, session_id, session_data, options)
|
217
|
+
end.to raise_error(RedisClient::CannotConnectError)
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
describe 'checking for session existence' do
|
224
|
+
let(:public_id) { 'foo' }
|
225
|
+
let(:session_id) { Rack::Session::SessionId.new(public_id) }
|
226
|
+
|
227
|
+
before do
|
228
|
+
allow(store).to receive(:current_session_id)
|
229
|
+
.with(:env).and_return(session_id)
|
230
|
+
end
|
231
|
+
|
232
|
+
context 'when session id is not provided' do
|
233
|
+
context 'when session id is nil' do
|
234
|
+
let(:session_id) { nil }
|
235
|
+
|
236
|
+
it 'returns false' do
|
237
|
+
expect(store.send(:session_exists?, :env)).to eq(false)
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
context 'when session id is empty string' do
|
242
|
+
let(:public_id) { '' }
|
243
|
+
|
244
|
+
it 'returns false' do
|
245
|
+
expect(store.send(:session_exists?, :env)).to eq(false)
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
context 'when session id is provided' do
|
251
|
+
let(:redis) do
|
252
|
+
double('redis').tap do |o|
|
253
|
+
allow(store).to receive(:redis).and_return(o)
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
context 'when session private id does not exist in redis' do
|
258
|
+
context 'when session public id does not exist in redis' do
|
259
|
+
it 'returns false' do
|
260
|
+
expect(redis).to receive(:call)
|
261
|
+
.with('EXISTS', session_id.private_id)
|
262
|
+
.and_return(0)
|
263
|
+
expect(redis).to receive(:call).with('EXISTS', 'foo').and_return(0)
|
264
|
+
expect(store.send(:session_exists?, :env)).to eq(false)
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
context 'when session public id exists in redis' do
|
269
|
+
it 'returns true' do
|
270
|
+
expect(redis).to receive(:call)
|
271
|
+
.with('EXISTS', session_id.private_id)
|
272
|
+
.and_return(0)
|
273
|
+
expect(redis).to receive(:call).with('EXISTS', 'foo').and_return(1)
|
274
|
+
expect(store.send(:session_exists?, :env)).to eq(true)
|
275
|
+
end
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
context 'when session private id exists in redis' do
|
280
|
+
it 'returns true' do
|
281
|
+
expect(redis).to receive(:call)
|
282
|
+
.with('EXISTS', session_id.private_id)
|
283
|
+
.and_return(1)
|
284
|
+
expect(store.send(:session_exists?, :env)).to eq(true)
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
context 'when session public id is formatted like a private id' do
|
289
|
+
let(:public_id) { Rack::Session::SessionId.new('foo').private_id }
|
290
|
+
|
291
|
+
it 'returns false' do
|
292
|
+
expect(redis).not_to receive(:call)
|
293
|
+
expect(store.send(:session_exists?, :env)).to eq(false)
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
context 'when redis is down' do
|
298
|
+
it 'returns true (fallback to old behavior)' do
|
299
|
+
allow(store).to receive(:redis).and_raise(RedisClient::CannotConnectError)
|
300
|
+
expect(store.send(:session_exists?, :env)).to eq(true)
|
301
|
+
end
|
302
|
+
end
|
303
|
+
end
|
304
|
+
end
|
305
|
+
|
306
|
+
describe 'fetching a session' do
|
307
|
+
let :options do
|
308
|
+
{
|
309
|
+
key_prefix: 'customprefix::'
|
310
|
+
}
|
311
|
+
end
|
312
|
+
|
313
|
+
let(:fake_key) { 'thisisarediskey' }
|
314
|
+
let(:session_id) { Rack::Session::SessionId.new(fake_key) }
|
315
|
+
|
316
|
+
describe 'generate_sid' do
|
317
|
+
it 'generates a secure ID' do
|
318
|
+
sid = store.send(:generate_sid)
|
319
|
+
expect(sid).to be_a(Rack::Session::SessionId)
|
320
|
+
end
|
321
|
+
end
|
322
|
+
|
323
|
+
context 'when redis is up' do
|
324
|
+
let(:redis) { double('redis') }
|
325
|
+
let(:private_exists) { 1 }
|
326
|
+
|
327
|
+
before do
|
328
|
+
allow(store).to receive(:redis).and_return(redis)
|
329
|
+
allow(redis).to receive(:call)
|
330
|
+
.with('EXISTS', "#{options[:key_prefix]}#{session_id.private_id}")
|
331
|
+
.and_return(private_exists)
|
332
|
+
end
|
333
|
+
|
334
|
+
context 'when session private id exists in redis' do
|
335
|
+
it 'retrieves the prefixed private id from redis' do
|
336
|
+
expect(redis).to receive(:call).with('GET',
|
337
|
+
"#{options[:key_prefix]}#{session_id.private_id}")
|
338
|
+
|
339
|
+
store.send(:get_session, double('env'), session_id)
|
340
|
+
end
|
341
|
+
end
|
342
|
+
|
343
|
+
context 'when session private id not found in redis' do
|
344
|
+
let(:private_exists) { 0 }
|
345
|
+
|
346
|
+
it 'retrieves the prefixed public id from redis' do
|
347
|
+
expect(redis).to receive(:call).with('GET', "#{options[:key_prefix]}#{fake_key}")
|
348
|
+
|
349
|
+
store.send(:get_session, double('env'), session_id)
|
350
|
+
end
|
351
|
+
end
|
352
|
+
|
353
|
+
context 'when session id is formatted like a private id' do
|
354
|
+
let(:fake_key) { Rack::Session::SessionId.new('anykey').private_id }
|
355
|
+
let(:new_sid) { Rack::Session::SessionId.new('newid') }
|
356
|
+
|
357
|
+
before do
|
358
|
+
allow(store).to receive(:generate_sid).and_return(new_sid)
|
359
|
+
end
|
360
|
+
|
361
|
+
it 'returns a default new session' do
|
362
|
+
expect(redis).not_to receive(:call)
|
363
|
+
expect(redis).not_to receive(:get)
|
364
|
+
expect(store.send(:get_session, double('env'), session_id))
|
365
|
+
.to eq([new_sid, {}])
|
366
|
+
end
|
367
|
+
end
|
368
|
+
end
|
369
|
+
|
370
|
+
context 'when redis is down' do
|
371
|
+
before do
|
372
|
+
allow(store).to receive(:redis).and_raise(RedisClient::CannotConnectError)
|
373
|
+
allow(store).to receive(:generate_sid).and_return('foop')
|
374
|
+
end
|
375
|
+
|
376
|
+
it 'returns an empty session hash' do
|
377
|
+
expect(store.send(:get_session, double('env'), session_id).last)
|
378
|
+
.to eq({})
|
379
|
+
end
|
380
|
+
|
381
|
+
it 'returns a newly generated sid' do
|
382
|
+
expect(store.send(:get_session, double('env'), session_id).first)
|
383
|
+
.to eq('foop')
|
384
|
+
end
|
385
|
+
|
386
|
+
context 'when :on_redis_down re-raises' do
|
387
|
+
before { store.on_redis_down = ->(e, *) { raise e } }
|
388
|
+
|
389
|
+
it 'explodes' do
|
390
|
+
expect do
|
391
|
+
store.send(:get_session, double('env'), session_id)
|
392
|
+
end.to raise_error(RedisClient::CannotConnectError)
|
393
|
+
end
|
394
|
+
end
|
395
|
+
end
|
396
|
+
end
|
397
|
+
|
398
|
+
describe 'destroying a session' do
|
399
|
+
context 'when the key is in the cookie hash' do
|
400
|
+
let(:env) { { 'rack.request.cookie_hash' => cookie_hash } }
|
401
|
+
let(:cookie_hash) { double('cookie hash') }
|
402
|
+
let(:fake_key) { 'thisisarediskey' }
|
403
|
+
let(:session_id) { Rack::Session::SessionId.new(fake_key) }
|
404
|
+
|
405
|
+
before do
|
406
|
+
allow(cookie_hash).to receive(:[]).and_return(fake_key)
|
407
|
+
end
|
408
|
+
|
409
|
+
it 'deletes the prefixed key from redis' do
|
410
|
+
redis = double('redis')
|
411
|
+
allow(store).to receive(:redis).and_return(redis)
|
412
|
+
expect(redis).to receive(:call).with('DEL', "#{options[:key_prefix]}#{fake_key}")
|
413
|
+
expect(redis).to receive(:call).with('DEL',
|
414
|
+
"#{options[:key_prefix]}#{session_id.private_id}")
|
415
|
+
|
416
|
+
store.send(:destroy, env)
|
417
|
+
end
|
418
|
+
|
419
|
+
context 'when redis is down' do
|
420
|
+
before do
|
421
|
+
allow(store).to receive(:redis).and_raise(RedisClient::CannotConnectError)
|
422
|
+
end
|
423
|
+
|
424
|
+
it 'returns false' do
|
425
|
+
expect(store.send(:destroy, env)).to eq(false)
|
426
|
+
end
|
427
|
+
|
428
|
+
context 'when :on_redis_down re-raises' do
|
429
|
+
before { store.on_redis_down = ->(e, *) { raise e } }
|
430
|
+
|
431
|
+
it 'explodes' do
|
432
|
+
expect do
|
433
|
+
store.send(:destroy, env)
|
434
|
+
end.to raise_error(RedisClient::CannotConnectError)
|
435
|
+
end
|
436
|
+
end
|
437
|
+
end
|
438
|
+
end
|
439
|
+
|
440
|
+
context 'when destroyed via #destroy_session' do
|
441
|
+
it 'deletes the prefixed key from redis' do
|
442
|
+
redis = redis_double
|
443
|
+
allow(store).to receive(:redis).and_return(redis)
|
444
|
+
sid = store.send(:generate_sid)
|
445
|
+
expect(redis).to receive(:call).with('DEL', "#{options[:key_prefix]}#{sid.public_id}")
|
446
|
+
expect(redis).to receive(:call).with('DEL', "#{options[:key_prefix]}#{sid.private_id}")
|
447
|
+
|
448
|
+
store.send(:destroy_session, {}, sid, nil)
|
449
|
+
end
|
450
|
+
end
|
451
|
+
end
|
452
|
+
|
453
|
+
describe 'session encoding' do
|
454
|
+
let(:env) { double('env') }
|
455
|
+
let(:session_id) { Rack::Session::SessionId.new('12 345') }
|
456
|
+
let(:session_data) { { 'some' => 'data' } }
|
457
|
+
let(:options) { {} }
|
458
|
+
let(:encoded_data) { Marshal.dump(session_data) }
|
459
|
+
let(:redis) { redis_double(set: nil, get: encoded_data) }
|
460
|
+
let(:expected_encoding) { encoded_data }
|
461
|
+
|
462
|
+
before do
|
463
|
+
allow(store).to receive(:redis).and_return(redis)
|
464
|
+
end
|
465
|
+
|
466
|
+
shared_examples_for 'serializer' do
|
467
|
+
it 'encodes correctly' do
|
468
|
+
expect(redis).to receive(:call).with('SET', session_id.private_id, expected_encoding)
|
469
|
+
store.send(:set_session, env, session_id, session_data, options)
|
470
|
+
end
|
471
|
+
|
472
|
+
it 'decodes correctly' do
|
473
|
+
allow(redis).to receive(:call).with('EXISTS', session_id.private_id).and_return(1)
|
474
|
+
expect(store.send(:get_session, env, session_id)).to eq([session_id, session_data])
|
475
|
+
end
|
476
|
+
end
|
477
|
+
|
478
|
+
context 'marshal' do
|
479
|
+
let(:options) { { serializer: :marshal } }
|
480
|
+
|
481
|
+
it_behaves_like 'serializer'
|
482
|
+
end
|
483
|
+
|
484
|
+
context 'json' do
|
485
|
+
let(:options) { { serializer: :json } }
|
486
|
+
let(:encoded_data) { '{"some":"data"}' }
|
487
|
+
|
488
|
+
it_behaves_like 'serializer'
|
489
|
+
end
|
490
|
+
|
491
|
+
context 'hybrid' do
|
492
|
+
let(:options) { { serializer: :hybrid } }
|
493
|
+
let(:expected_encoding) { '{"some":"data"}' }
|
494
|
+
|
495
|
+
context 'marshal encoded data' do
|
496
|
+
it_behaves_like 'serializer'
|
497
|
+
end
|
498
|
+
|
499
|
+
context 'json encoded data' do
|
500
|
+
let(:encoded_data) { '{"some":"data"}' }
|
501
|
+
|
502
|
+
it_behaves_like 'serializer'
|
503
|
+
end
|
504
|
+
end
|
505
|
+
|
506
|
+
context 'custom' do
|
507
|
+
let :custom_serializer do
|
508
|
+
Class.new do
|
509
|
+
def self.load(_value)
|
510
|
+
{ 'some' => 'data' }
|
511
|
+
end
|
512
|
+
|
513
|
+
def self.dump(_value)
|
514
|
+
'somedata'
|
515
|
+
end
|
516
|
+
end
|
517
|
+
end
|
518
|
+
|
519
|
+
let(:options) { { serializer: custom_serializer } }
|
520
|
+
let(:expected_encoding) { 'somedata' }
|
521
|
+
|
522
|
+
it_behaves_like 'serializer'
|
523
|
+
end
|
524
|
+
end
|
525
|
+
|
526
|
+
describe 'handling decode errors' do
|
527
|
+
context 'when a class is serialized that does not exist' do
|
528
|
+
before do
|
529
|
+
allow(store).to receive(:redis)
|
530
|
+
.and_return(redis_double(
|
531
|
+
get: "\x04\bo:\nNonExistentClass\x00",
|
532
|
+
del: true
|
533
|
+
))
|
534
|
+
end
|
535
|
+
|
536
|
+
it 'returns an empty session' do
|
537
|
+
expect(store.send(:load_session_from_redis, 'whatever')).to be_nil
|
538
|
+
end
|
539
|
+
|
540
|
+
it 'destroys and drops the session' do
|
541
|
+
expect(store).to receive(:destroy_session_from_sid)
|
542
|
+
.with('wut', drop: true)
|
543
|
+
store.send(:load_session_from_redis, 'wut')
|
544
|
+
end
|
545
|
+
|
546
|
+
context 'when a custom on_session_load_error handler is provided' do
|
547
|
+
before do
|
548
|
+
store.on_session_load_error = lambda do |e, sid|
|
549
|
+
@e = e
|
550
|
+
@sid = sid
|
551
|
+
end
|
552
|
+
end
|
553
|
+
|
554
|
+
it 'passes the error and the sid to the handler' do
|
555
|
+
store.send(:load_session_from_redis, 'foo')
|
556
|
+
expect(@e).to be_kind_of(StandardError)
|
557
|
+
expect(@sid).to eq('foo')
|
558
|
+
end
|
559
|
+
end
|
560
|
+
end
|
561
|
+
|
562
|
+
context 'when the encoded data is invalid' do
|
563
|
+
before do
|
564
|
+
allow(store).to receive(:redis)
|
565
|
+
.and_return(redis_double(get: "\x00\x00\x00\x00", del: true))
|
566
|
+
end
|
567
|
+
|
568
|
+
it 'returns an empty session' do
|
569
|
+
expect(store.send(:load_session_from_redis, 'bar')).to be_nil
|
570
|
+
end
|
571
|
+
|
572
|
+
it 'destroys and drops the session' do
|
573
|
+
expect(store).to receive(:destroy_session_from_sid)
|
574
|
+
.with('wut', drop: true)
|
575
|
+
store.send(:load_session_from_redis, 'wut')
|
576
|
+
end
|
577
|
+
|
578
|
+
context 'when a custom on_session_load_error handler is provided' do
|
579
|
+
before do
|
580
|
+
store.on_session_load_error = lambda do |e, sid|
|
581
|
+
@e = e
|
582
|
+
@sid = sid
|
583
|
+
end
|
584
|
+
end
|
585
|
+
|
586
|
+
it 'passes the error and the sid to the handler' do
|
587
|
+
store.send(:load_session_from_redis, 'foo')
|
588
|
+
expect(@e).to be_kind_of(StandardError)
|
589
|
+
expect(@sid).to eq('foo')
|
590
|
+
end
|
591
|
+
end
|
592
|
+
end
|
593
|
+
end
|
594
|
+
|
595
|
+
describe 'validating custom handlers' do
|
596
|
+
%w(on_redis_down on_session_load_error).each do |h|
|
597
|
+
context 'when nil' do
|
598
|
+
it 'does not explode at init' do
|
599
|
+
expect { store }.not_to raise_error
|
600
|
+
end
|
601
|
+
end
|
602
|
+
|
603
|
+
context 'when callable' do
|
604
|
+
let(:options) { { "#{h}": ->(*) { true } } }
|
605
|
+
|
606
|
+
it 'does not explode at init' do
|
607
|
+
expect { store }.not_to raise_error
|
608
|
+
end
|
609
|
+
end
|
610
|
+
|
611
|
+
context 'when not callable' do
|
612
|
+
let(:options) { { "#{h}": 'herpderp' } }
|
613
|
+
|
614
|
+
it 'explodes at init' do
|
615
|
+
expect { store }.to raise_error(ArgumentError)
|
616
|
+
end
|
617
|
+
end
|
618
|
+
end
|
619
|
+
end
|
620
|
+
|
621
|
+
describe 'setting the session' do
|
622
|
+
it 'allows changing the session' do
|
623
|
+
env = { 'rack.session.options' => {} }
|
624
|
+
sid = Rack::Session::SessionId.new('1234')
|
625
|
+
allow(store).to receive(:redis).and_return(RedisClient.new(url: ENV['REDIS_URL']))
|
626
|
+
data1 = { 'foo' => 'bar' }
|
627
|
+
store.send(:set_session, env, sid, data1)
|
628
|
+
data2 = { 'baz' => 'wat' }
|
629
|
+
store.send(:set_session, env, sid, data2)
|
630
|
+
_, session = store.send(:get_session, env, sid)
|
631
|
+
expect(session).to eq(data2)
|
632
|
+
end
|
633
|
+
|
634
|
+
it 'allows changing the session when the session has an expiry' do
|
635
|
+
env = { 'rack.session.options' => { expire_after: 60 } }
|
636
|
+
sid = Rack::Session::SessionId.new('1234')
|
637
|
+
allow(store).to receive(:redis).and_return(RedisClient.new(url: ENV['REDIS_URL']))
|
638
|
+
data1 = { 'foo' => 'bar' }
|
639
|
+
store.send(:set_session, env, sid, data1)
|
640
|
+
data2 = { 'baz' => 'wat' }
|
641
|
+
store.send(:set_session, env, sid, data2)
|
642
|
+
_, session = store.send(:get_session, env, sid)
|
643
|
+
expect(session).to eq(data2)
|
644
|
+
end
|
645
|
+
end
|
646
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'simplecov'
|
2
|
+
require_relative 'support'
|
3
|
+
require 'redis-client-session-store'
|
4
|
+
|
5
|
+
# from git submodule
|
6
|
+
require_relative 'support/redis-client/test/support/servers'
|
7
|
+
require_relative 'support/redis_double'
|
8
|
+
|
9
|
+
unless ENV['REDIS_URL']
|
10
|
+
class TestRedisManager < Servers::RedisManager
|
11
|
+
# workaround for "Failed opening Unix socket: unix socket path too long"
|
12
|
+
def command
|
13
|
+
super - ['--unixsocket', Servers::REDIS_SOCKET_FILE.to_s]
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
Dir.mkdir('tmp') unless Dir.exist?('tmp')
|
18
|
+
Servers.build_redis
|
19
|
+
REDIS = TestRedisManager.new(
|
20
|
+
'redis',
|
21
|
+
port: 16_380,
|
22
|
+
real_port: 16_380
|
23
|
+
)
|
24
|
+
TEST_SERVERS = ServerList.new(REDIS)
|
25
|
+
TEST_SERVERS.prepare
|
26
|
+
|
27
|
+
ENV['REDIS_URL'] = "redis://#{REDIS.host}:#{REDIS.port}"
|
28
|
+
|
29
|
+
RSpec.configure do |config|
|
30
|
+
config.after(:suite) { TEST_SERVERS.shutdown }
|
31
|
+
end
|
32
|
+
end
|