mongo_rack 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/HISTORY +2 -0
- data/README.rdoc +61 -0
- data/Rakefile +31 -0
- data/fred.rb +26 -0
- data/lib/core_ext/hash.rb +6 -0
- data/lib/mongo_rack.rb +159 -0
- data/lib/mongo_rack/session_hash.rb +144 -0
- data/spec/mongo_rack_spec.rb +207 -0
- data/spec/session_hash_spec.rb +42 -0
- data/spec/spec_helper.rb +20 -0
- data/tasks/bones.rake +20 -0
- data/tasks/gem.rake +202 -0
- data/tasks/git.rake +40 -0
- data/tasks/notes.rake +27 -0
- data/tasks/post_load.rake +32 -0
- data/tasks/rdoc.rake +54 -0
- data/tasks/setup.rb +292 -0
- data/tasks/spec.rake +54 -0
- data/tasks/svn.rake +47 -0
- metadata +115 -0
data/HISTORY
ADDED
data/README.rdoc
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
== mongo_rack
|
2
|
+
|
3
|
+
mongoDB based session management
|
4
|
+
|
5
|
+
== DESCRIPTION:
|
6
|
+
|
7
|
+
A mongoDB based rackable session store. Can be used with any rack based web frameworks.
|
8
|
+
|
9
|
+
== PROJECT INFORMATION
|
10
|
+
|
11
|
+
* Developer: Fernand Galiana
|
12
|
+
* Git: git://github.com/derailed/mongo-rack.git
|
13
|
+
|
14
|
+
== REQUIREMENTS:
|
15
|
+
|
16
|
+
* rack 1.0 or later
|
17
|
+
* mongo + mongo_ext ruby adapter 0.18 or later
|
18
|
+
|
19
|
+
== INSTALL:
|
20
|
+
|
21
|
+
* sudo gem install mongo_rack
|
22
|
+
|
23
|
+
== USAGE:
|
24
|
+
|
25
|
+
use Rack::Session::Mongo, { :server => 'localhost:27017/mongo_ses/sessions }
|
26
|
+
|
27
|
+
where server requires the following format
|
28
|
+
|
29
|
+
{server_name_or_ip}:{port}/{database_name}/{collection_name}
|
30
|
+
|
31
|
+
The server description by default will be localhost:2701/mongo_session/sessions
|
32
|
+
|
33
|
+
Other options includes:
|
34
|
+
|
35
|
+
pool_size ( default is 1 )
|
36
|
+
pool_timeout ( defaults to 1 )
|
37
|
+
|
38
|
+
== LICENSE:
|
39
|
+
|
40
|
+
(The MIT License)
|
41
|
+
|
42
|
+
Copyright (c) 2009
|
43
|
+
|
44
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
45
|
+
a copy of this software and associated documentation files (the
|
46
|
+
'Software'), to deal in the Software without restriction, including
|
47
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
48
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
49
|
+
permit persons to whom the Software is furnished to do so, subject to
|
50
|
+
the following conditions:
|
51
|
+
|
52
|
+
The above copyright notice and this permission notice shall be
|
53
|
+
included in all copies or substantial portions of the Software.
|
54
|
+
|
55
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
56
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
57
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
58
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
59
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
60
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
61
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
begin
|
2
|
+
require 'bones'
|
3
|
+
Bones.setup
|
4
|
+
rescue LoadError
|
5
|
+
begin
|
6
|
+
load 'tasks/setup.rb'
|
7
|
+
rescue LoadError
|
8
|
+
raise RuntimeError, '### please install the "bones" gem ###'
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
ensure_in_path 'lib'
|
13
|
+
require 'mongo_rack'
|
14
|
+
|
15
|
+
task :default => 'spec:run'
|
16
|
+
|
17
|
+
PROJ.name = 'mongo_rack'
|
18
|
+
PROJ.authors = 'Fernand Galiana'
|
19
|
+
PROJ.email = 'fernand.galiana@gmail.com'
|
20
|
+
PROJ.url = 'http://github.com/derailed/mongo_rack'
|
21
|
+
PROJ.summary = "Rackable mongoDB based session management"
|
22
|
+
PROJ.version = "0.0.1"
|
23
|
+
PROJ.ruby_opts = %w[-W0]
|
24
|
+
PROJ.readme = 'README.rdoc'
|
25
|
+
PROJ.rcov.opts = ["--sort", "coverage", "-T"]
|
26
|
+
PROJ.spec.opts << '--color'
|
27
|
+
|
28
|
+
# Dependencies
|
29
|
+
depend_on "rack" , ">= 1.0.0"
|
30
|
+
depend_on "mongo" , ">= 0.18.1"
|
31
|
+
depend_on "mongo_ext", ">= 0.18.1"
|
data/fred.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# tnum = 10
|
2
|
+
# r = Array.new(tnum) do
|
3
|
+
# Thread.new do
|
4
|
+
# puts "Making request"
|
5
|
+
# 10
|
6
|
+
# end
|
7
|
+
# end.reverse.map{|t| puts t.inspect;t.join; puts t.value }
|
8
|
+
#
|
9
|
+
# r.each do |res|
|
10
|
+
# puts "Checking request #{res}"
|
11
|
+
# end
|
12
|
+
|
13
|
+
|
14
|
+
a = Array.new( 10 ) do
|
15
|
+
Thread.new( 20 ) do |run|
|
16
|
+
puts "Blee + #{run}"
|
17
|
+
20
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
puts a.inspect
|
22
|
+
|
23
|
+
b = a.reverse.map{ |t| t.join.value }
|
24
|
+
|
25
|
+
puts "Here"
|
26
|
+
puts b.inspect
|
data/lib/mongo_rack.rb
ADDED
@@ -0,0 +1,159 @@
|
|
1
|
+
require 'rack/session/abstract/id'
|
2
|
+
require 'mongo'
|
3
|
+
require File.join( File.dirname(__FILE__), %w[mongo_rack session_hash.rb] )
|
4
|
+
|
5
|
+
module Rack
|
6
|
+
module Session
|
7
|
+
class Mongo < Abstract::ID
|
8
|
+
attr_reader :mutex, :connection, :db, :sessions #:nodoc:
|
9
|
+
|
10
|
+
# === Options for mongo_rack
|
11
|
+
# :server ::
|
12
|
+
# Specifies server, port, db and collection location. Defaults
|
13
|
+
# to localhost:27017/mongo_session/sessions. Format must conform to
|
14
|
+
# the format {host}:{port}/{database_name}/{collection_name}.
|
15
|
+
# :pool_size ::
|
16
|
+
# The connection socket pool size - see mongo-ruby-driver docs for settings.
|
17
|
+
# Defaults to 1 connection.
|
18
|
+
# :pool_timeout ::
|
19
|
+
# The connection pool timeout. see mongo-ruby-driver docs for settings.
|
20
|
+
# Defaults to 1 sec.
|
21
|
+
DEFAULT_OPTIONS = Abstract::ID::DEFAULT_OPTIONS.merge \
|
22
|
+
:server => 'localhost:27017/mongo_session/sessions',
|
23
|
+
:pool_size => 1,
|
24
|
+
:pool_timeout => 1.0
|
25
|
+
|
26
|
+
# Initializes mongo_rack. Pass in options for default override.
|
27
|
+
def initialize(app, options={})
|
28
|
+
super
|
29
|
+
|
30
|
+
host, port, db_name, cltn_name = parse_server_desc( @default_options[:server] )
|
31
|
+
|
32
|
+
@mutex = Mutex.new
|
33
|
+
@connection = ::Mongo::Connection.new( host, port,
|
34
|
+
:pool_size => @default_options[:pool_size],
|
35
|
+
:timeout => @default_options[:pool_timeout] )
|
36
|
+
@db = @connection.db( db_name )
|
37
|
+
@sessions = @db[cltn_name]
|
38
|
+
end
|
39
|
+
|
40
|
+
# Fetch session with optional session id. Retrieve session from mongodb if any
|
41
|
+
def get_session( env, sid )
|
42
|
+
return _get_session( env, sid ) unless env['rack.multithread']
|
43
|
+
mutex.synchronize do
|
44
|
+
return _get_session( env, sid )
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Update session params and sync to mongoDB.
|
49
|
+
def set_session( env, sid, new_session, options )
|
50
|
+
return _set_session( env, sid, new_session, options ) unless env['rack.multithread']
|
51
|
+
mutex.synchronize do
|
52
|
+
return _set_session( env, sid, new_session, options )
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# =======================================================================
|
57
|
+
private
|
58
|
+
|
59
|
+
# Generates unique session id
|
60
|
+
def generate_sid
|
61
|
+
loop do
|
62
|
+
sid = super
|
63
|
+
break sid unless sessions.find_one( { :_id => sid } )
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Check session expiration date
|
68
|
+
def fresh?( ses_obj )
|
69
|
+
return true if ses_obj['expire'] == 0
|
70
|
+
now = Time.now
|
71
|
+
ses_obj['expire'] >= now
|
72
|
+
end
|
73
|
+
|
74
|
+
# Clean out all expired sessions
|
75
|
+
def clean_expired!
|
76
|
+
sessions.remove( { :expire => { '$lt' => Time.now } } )
|
77
|
+
end
|
78
|
+
|
79
|
+
# parse server description string into host, port, db, cltn
|
80
|
+
def parse_server_desc( desc )
|
81
|
+
tokens = desc.split( "/" )
|
82
|
+
raise "Invalid server description" unless tokens.size == 3
|
83
|
+
server_desc = tokens[0].split( ":" )
|
84
|
+
raise "Invalid host:port description" unless server_desc.size == 2
|
85
|
+
return server_desc.first, server_desc.last.to_i, tokens[1], tokens[2]
|
86
|
+
end
|
87
|
+
|
88
|
+
# fetch session with optional session id
|
89
|
+
def _get_session(env, sid)
|
90
|
+
if sid
|
91
|
+
ses_obj = sessions.find_one( { :_id => sid } )
|
92
|
+
session = MongoRack::SessionHash.new( ses_obj['data'] ) if ses_obj and fresh?( ses_obj )
|
93
|
+
end
|
94
|
+
|
95
|
+
unless sid and session
|
96
|
+
env['rack.errors'].puts("Session '#{sid.inspect}' not found, initializing...") if $VERBOSE and not sid.nil?
|
97
|
+
session = {}
|
98
|
+
sid = generate_sid
|
99
|
+
ret = sessions.save( { :_id => sid, :data => session } )
|
100
|
+
raise "Session collision on '#{sid.inspect}'" unless ret
|
101
|
+
end
|
102
|
+
session.instance_variable_set( '@old', MongoRack::SessionHash.new.merge(session) )
|
103
|
+
return [sid, session]
|
104
|
+
rescue => boom
|
105
|
+
warn "#{self} is unable to find server."
|
106
|
+
warn $!.inspect
|
107
|
+
return [ nil, {} ]
|
108
|
+
end
|
109
|
+
|
110
|
+
# update session information with new settings
|
111
|
+
def _set_session(env, sid, new_session, options)
|
112
|
+
ses_obj = sessions.find_one( { :_id => sid } )
|
113
|
+
if ses_obj
|
114
|
+
session = MongoRack::SessionHash.new( ses_obj['data'] )
|
115
|
+
else
|
116
|
+
session = MongoRack::SessionHash.new
|
117
|
+
end
|
118
|
+
|
119
|
+
if options[:renew] or options[:drop]
|
120
|
+
sessions.remove( { :_id => sid } )
|
121
|
+
return false if options[:drop]
|
122
|
+
sid = generate_sid
|
123
|
+
sessions.insert( {:_id => sid, :data => {} } )
|
124
|
+
end
|
125
|
+
old_session = new_session.instance_variable_get('@old') || MongoRack::SessionHash.new
|
126
|
+
merged = merge_sessions( sid, old_session, new_session, session )
|
127
|
+
|
128
|
+
expiry = options[:expire_after]
|
129
|
+
expiry = expiry ? Time.now + options[:expire_after] : 0
|
130
|
+
|
131
|
+
# BOZO ! Use upserts here if minor changes ?
|
132
|
+
sessions.save( { :_id => sid, :data => merged, :expire => expiry } )
|
133
|
+
return sid
|
134
|
+
rescue => boom
|
135
|
+
warn "#{self} is unable to find server."
|
136
|
+
warn $!.inspect
|
137
|
+
return false
|
138
|
+
end
|
139
|
+
|
140
|
+
# merge old, new to current session state
|
141
|
+
def merge_sessions( sid, old_s, new_s, cur={} )
|
142
|
+
unless Hash === old_s and Hash === new_s
|
143
|
+
warn 'Bad old or new sessions provided.'
|
144
|
+
return cur
|
145
|
+
end
|
146
|
+
|
147
|
+
delete = old_s.keys - new_s.keys
|
148
|
+
warn "//@#{sid}: delete #{delete*','}" if $VERBOSE and not delete.empty?
|
149
|
+
delete.each{ |k| cur.delete(k) }
|
150
|
+
|
151
|
+
update = new_s.keys.select{ |k| new_s[k] != old_s[k] }
|
152
|
+
warn "//@#{sid}: update #{update*','}" if $VERBOSE and not update.empty?
|
153
|
+
update.each{ |k| cur[k] = new_s[k] }
|
154
|
+
|
155
|
+
cur
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
@@ -0,0 +1,144 @@
|
|
1
|
+
# Snagged HashWithIndifferentAccess for A/S
|
2
|
+
module MongoRack
|
3
|
+
class SessionHash < Hash
|
4
|
+
|
5
|
+
# Need to enable users to access session using either a symbol or a string as key
|
6
|
+
# This call wraps hash to provide this kind of access. No default allowed here. If a key
|
7
|
+
# is not found nil will be returned.
|
8
|
+
def initialize(constructor = {})
|
9
|
+
if constructor.is_a?(Hash)
|
10
|
+
super(constructor)
|
11
|
+
update(constructor)
|
12
|
+
self.default = nil
|
13
|
+
else
|
14
|
+
super(constructor)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# Checks for default value. If key does not exits returns default for hash
|
19
|
+
def default(key = nil)
|
20
|
+
if key.is_a?(Symbol) && include?(key = key.to_s)
|
21
|
+
self[key]
|
22
|
+
else
|
23
|
+
super
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
alias_method :regular_writer, :[]= unless method_defined?(:regular_writer)
|
28
|
+
alias_method :regular_update, :update unless method_defined?(:regular_update)
|
29
|
+
|
30
|
+
# Assigns a new value to the hash:
|
31
|
+
#
|
32
|
+
# hash = SessionHash.new
|
33
|
+
# hash[:key] = "value"
|
34
|
+
#
|
35
|
+
def []=(key, value)
|
36
|
+
regular_writer(convert_key(key), convert_value(value))
|
37
|
+
end
|
38
|
+
|
39
|
+
# Updates the instantized hash with values from the second:
|
40
|
+
#
|
41
|
+
# hash_1 = SessionHash.new
|
42
|
+
# hash_1[:key] = "value"
|
43
|
+
#
|
44
|
+
# hash_2 = SessionHash.new
|
45
|
+
# hash_2[:key] = "New Value!"
|
46
|
+
#
|
47
|
+
# hash_1.update(hash_2) # => {"key"=>"New Value!"}
|
48
|
+
#
|
49
|
+
def update(other_hash)
|
50
|
+
other_hash.each_pair { |key, value| regular_writer(convert_key(key), convert_value(value)) }
|
51
|
+
self
|
52
|
+
end
|
53
|
+
|
54
|
+
alias_method :merge!, :update
|
55
|
+
|
56
|
+
# Checks the hash for a key matching the argument passed in:
|
57
|
+
#
|
58
|
+
# hash = SessionHash.new
|
59
|
+
# hash["key"] = "value"
|
60
|
+
# hash.key? :key # => true
|
61
|
+
# hash.key? "key" # => true
|
62
|
+
#
|
63
|
+
def key?(key)
|
64
|
+
super(convert_key(key))
|
65
|
+
end
|
66
|
+
|
67
|
+
alias_method :include?, :key?
|
68
|
+
alias_method :has_key?, :key?
|
69
|
+
alias_method :member?, :key?
|
70
|
+
|
71
|
+
# Fetches the value for the specified key, same as doing hash[key]
|
72
|
+
def fetch(key, *extras)
|
73
|
+
super(convert_key(key), *extras)
|
74
|
+
end
|
75
|
+
|
76
|
+
# Returns an array of the values at the specified indices:
|
77
|
+
#
|
78
|
+
# hash = SessionHash.new
|
79
|
+
# hash[:a] = "x"
|
80
|
+
# hash[:b] = "y"
|
81
|
+
# hash.values_at("a", "b") # => ["x", "y"]
|
82
|
+
#
|
83
|
+
def values_at(*indices)
|
84
|
+
indices.collect {|key| self[convert_key(key)]}
|
85
|
+
end
|
86
|
+
|
87
|
+
# Returns an exact copy of the hash.
|
88
|
+
def dup
|
89
|
+
SessionHash.new(self)
|
90
|
+
end
|
91
|
+
|
92
|
+
# Merges the instantized and the specified hashes together, giving precedence to the values from the second hash
|
93
|
+
# Does not overwrite the existing hash.
|
94
|
+
def merge(hash)
|
95
|
+
self.dup.update(hash)
|
96
|
+
end
|
97
|
+
|
98
|
+
# Removes a specified key from the hash.
|
99
|
+
def delete(key)
|
100
|
+
super(convert_key(key))
|
101
|
+
end
|
102
|
+
|
103
|
+
#:nodoc:
|
104
|
+
def stringify_keys!; self end
|
105
|
+
#:nodoc:
|
106
|
+
def symbolize_keys!; self end
|
107
|
+
#:nodoc:
|
108
|
+
def to_options!; self end
|
109
|
+
|
110
|
+
# Convert to a Hash with String keys.
|
111
|
+
def to_hash
|
112
|
+
Hash.new(default).merge(self)
|
113
|
+
end
|
114
|
+
|
115
|
+
# =========================================================================
|
116
|
+
private
|
117
|
+
|
118
|
+
# converts key to string if symbol
|
119
|
+
def convert_key(key)
|
120
|
+
key.kind_of?(Symbol) ? key.to_s : key
|
121
|
+
end
|
122
|
+
|
123
|
+
# check value and converts sub obj to session hash if any
|
124
|
+
def convert_value(value)
|
125
|
+
case value
|
126
|
+
when Hash
|
127
|
+
value.with_session_access
|
128
|
+
when Array
|
129
|
+
value.collect { |e| e.is_a?(Hash) ? e.with_session_access : e }
|
130
|
+
else
|
131
|
+
value
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
module MongoRack
|
138
|
+
module SessionAccess
|
139
|
+
def with_session_access
|
140
|
+
hash = MongoRack::SessionHash.new( self )
|
141
|
+
hash
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
@@ -0,0 +1,207 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), %w[spec_helper])
|
2
|
+
|
3
|
+
describe Rack::Session::Mongo do
|
4
|
+
before :all do
|
5
|
+
@session_key = 'rack.session'
|
6
|
+
@session_match = /#{@session_key}=[0-9a-fA-F]+;/
|
7
|
+
@db_name = 'mongo_test'
|
8
|
+
@cltn_name = 'sessions'
|
9
|
+
|
10
|
+
@con = Mongo::Connection.new
|
11
|
+
@db = @con.db( @db_name )
|
12
|
+
@sessions = @db['sessions']
|
13
|
+
|
14
|
+
@incrementor = lambda do |env|
|
15
|
+
env[@session_key]['counter'] ||= 0
|
16
|
+
env[@session_key]['counter'] += 1
|
17
|
+
Rack::Response.new( env[@session_key].inspect ).to_a
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
it "should connect to a valid server" do
|
22
|
+
Rack::Session::Mongo.new( @incrementor, :server => "localhost:27017/#{@db_name}/#{@cltn_name}" )
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should fail if bad server specified" do
|
26
|
+
lambda do
|
27
|
+
Rack::Session::Mongo.new( @incrementor, :server => "blee:1111/#{@db_name}/#{@cltn_name}" )
|
28
|
+
end.should raise_error( Mongo::ConnectionFailure )
|
29
|
+
end
|
30
|
+
|
31
|
+
describe "cookies" do
|
32
|
+
before :each do
|
33
|
+
@pool = Rack::Session::Mongo.new( @incrementor, :server => "localhost:27017/#{@db_name}/#{@cltn_name}" )
|
34
|
+
end
|
35
|
+
|
36
|
+
it "should create a new cookie correctly" do
|
37
|
+
res = Rack::MockRequest.new( @pool ).get( "/", 'rack.multithread' => false )
|
38
|
+
res['Set-Cookie'].should match( /^#{@session_key}=/ )
|
39
|
+
res.body.should == '{"counter"=>1}'
|
40
|
+
session_id = res['Set-Cookie'].match( /^#{@session_key}=(.*?);.*?/ )[1]
|
41
|
+
mongo_check( res, :counter, 1 )
|
42
|
+
end
|
43
|
+
|
44
|
+
it "should determine a session from a cookie" do
|
45
|
+
req = Rack::MockRequest.new( @pool )
|
46
|
+
res = req.get("/", 'rack.multithread' => false )
|
47
|
+
cookie = res["Set-Cookie"]
|
48
|
+
req.get("/", "HTTP_COOKIE" => cookie, 'rack.multithread' => false ).body.should == '{"counter"=>2}'
|
49
|
+
mongo_check( res, :counter, 2 )
|
50
|
+
req.get("/", "HTTP_COOKIE" => cookie, 'rack.multithread' => false ).body.should == '{"counter"=>3}'
|
51
|
+
mongo_check( res, :counter, 3 )
|
52
|
+
end
|
53
|
+
|
54
|
+
it "survives nonexistant cookies" do
|
55
|
+
bad_cookie = "rack.session=bumblebeetuna"
|
56
|
+
res = Rack::MockRequest.new( @pool ).get("/", "HTTP_COOKIE" => bad_cookie, 'rack.multithread' => false )
|
57
|
+
res.body.should == '{"counter"=>1}'
|
58
|
+
cookie = res["Set-Cookie"][@session_match]
|
59
|
+
cookie.should_not match( /#{bad_cookie}/ )
|
60
|
+
end
|
61
|
+
|
62
|
+
it "maintains freshness" do
|
63
|
+
pool = Rack::Session::Mongo.new( @incrementor, :server => "localhost:27017/#{@db_name}/#{@cltn_name}", :expire_after => 1 )
|
64
|
+
res = Rack::MockRequest.new(pool).get('/', 'rack.multithread' => false )
|
65
|
+
res.body.should include('"counter"=>1')
|
66
|
+
cookie = res["Set-Cookie"]
|
67
|
+
res = Rack::MockRequest.new(pool).get('/', "HTTP_COOKIE" => cookie, 'rack.multithread' => false )
|
68
|
+
res["Set-Cookie"].should == cookie
|
69
|
+
res.body.should include('"counter"=>2')
|
70
|
+
puts 'Sleeping to expire session' if $DEBUG
|
71
|
+
sleep 2
|
72
|
+
res = Rack::MockRequest.new(pool).get('/', "HTTP_COOKIE" => cookie, 'rack.multithread' => false )
|
73
|
+
res["Set-Cookie"].should_not == cookie
|
74
|
+
res.body.should include( '"counter"=>1' )
|
75
|
+
end
|
76
|
+
|
77
|
+
it "deletes cookies with :drop option" do
|
78
|
+
drop_session = lambda do |env|
|
79
|
+
env['rack.session.options'][:drop] = true
|
80
|
+
@incrementor.call(env)
|
81
|
+
end
|
82
|
+
|
83
|
+
req = Rack::MockRequest.new(@pool)
|
84
|
+
drop = Rack::Utils::Context.new(@pool, drop_session)
|
85
|
+
dreq = Rack::MockRequest.new(drop)
|
86
|
+
|
87
|
+
res0 = req.get("/", 'rack.multithread' => false )
|
88
|
+
session = (cookie = res0["Set-Cookie"])[@session_match]
|
89
|
+
res0.body.should == '{"counter"=>1}'
|
90
|
+
|
91
|
+
res1 = req.get("/", "HTTP_COOKIE" => cookie, 'rack.multithread' => false )
|
92
|
+
res1["Set-Cookie"][@session_match].should == session
|
93
|
+
res1.body.should == '{"counter"=>2}'
|
94
|
+
|
95
|
+
res2 = dreq.get("/", "HTTP_COOKIE" => cookie, 'rack.multithread' => false )
|
96
|
+
res2["Set-Cookie"].should == nil
|
97
|
+
res2.body.should == '{"counter"=>3}'
|
98
|
+
|
99
|
+
res3 = req.get("/", "HTTP_COOKIE" => cookie, 'rack.multithread' => false)
|
100
|
+
res3["Set-Cookie"][@session_match].should_not == session
|
101
|
+
res3.body.should == '{"counter"=>1}'
|
102
|
+
end
|
103
|
+
|
104
|
+
it "provides new session id with :renew option" do
|
105
|
+
renew_session = lambda do |env|
|
106
|
+
env['rack.session.options'][:renew] = true
|
107
|
+
@incrementor.call(env)
|
108
|
+
end
|
109
|
+
|
110
|
+
req = Rack::MockRequest.new(@pool)
|
111
|
+
renew = Rack::Utils::Context.new(@pool, renew_session)
|
112
|
+
rreq = Rack::MockRequest.new(renew)
|
113
|
+
|
114
|
+
res0 = req.get("/", 'rack.multithread' => false )
|
115
|
+
session = (cookie = res0["Set-Cookie"])[@session_match]
|
116
|
+
res0.body.should == '{"counter"=>1}'
|
117
|
+
|
118
|
+
res1 = req.get("/", "HTTP_COOKIE" => cookie, 'rack.multithread' => false )
|
119
|
+
res1["Set-Cookie"][@session_match].should == session
|
120
|
+
res1.body.should == '{"counter"=>2}'
|
121
|
+
|
122
|
+
res2 = rreq.get("/", "HTTP_COOKIE" => cookie, 'rack.multithread' => false )
|
123
|
+
new_cookie = res2["Set-Cookie"]
|
124
|
+
new_session = new_cookie[@session_match]
|
125
|
+
new_session.should_not == session
|
126
|
+
res2.body.should == '{"counter"=>3}'
|
127
|
+
|
128
|
+
res3 = req.get("/", "HTTP_COOKIE" => new_cookie, 'rack.multithread' => false )
|
129
|
+
res3["Set-Cookie"][@session_match].should == new_session
|
130
|
+
res3.body.should == '{"counter"=>4}'
|
131
|
+
end
|
132
|
+
|
133
|
+
it "omits cookie with :defer option" do
|
134
|
+
defer_session = lambda do |env|
|
135
|
+
env['rack.session.options'][:defer] = true
|
136
|
+
@incrementor.call(env)
|
137
|
+
end
|
138
|
+
|
139
|
+
req = Rack::MockRequest.new(@pool)
|
140
|
+
defer = Rack::Utils::Context.new(@pool, defer_session)
|
141
|
+
dreq = Rack::MockRequest.new(defer)
|
142
|
+
|
143
|
+
res0 = req.get("/", 'rack.multithread' => false )
|
144
|
+
session = (cookie = res0["Set-Cookie"])[@session_match]
|
145
|
+
res0.body.should == '{"counter"=>1}'
|
146
|
+
|
147
|
+
res1 = req.get("/", "HTTP_COOKIE" => cookie, 'rack.multithread' => false )
|
148
|
+
res1["Set-Cookie"][@session_match].should == session
|
149
|
+
res1.body.should == '{"counter"=>2}'
|
150
|
+
|
151
|
+
res2 = dreq.get("/", "HTTP_COOKIE" => cookie, 'rack.multithread' => false )
|
152
|
+
res2["Set-Cookie"].should == nil
|
153
|
+
res2.body.should == '{"counter"=>3}'
|
154
|
+
|
155
|
+
res3 = req.get("/", "HTTP_COOKIE" => cookie, 'rack.multithread' => false )
|
156
|
+
res3["Set-Cookie"][@session_match].should == session
|
157
|
+
res3.body.should == '{"counter"=>4}'
|
158
|
+
end
|
159
|
+
|
160
|
+
it "multithread: should cleanly merge sessions" do
|
161
|
+
@pool = Rack::Session::Mongo.new( @incrementor, :server => "localhost:27017/#{@db_name}/#{@cltn_name}", :pool_size => 10 )
|
162
|
+
|
163
|
+
req = Rack::MockRequest.new( @pool )
|
164
|
+
|
165
|
+
res = req.get('/')
|
166
|
+
res.body.should == '{"counter"=>1}'
|
167
|
+
cookie = res["Set-Cookie"]
|
168
|
+
sess_id = cookie[/#{@pool.key}=([^,;]+)/,1]
|
169
|
+
|
170
|
+
r = Array.new( 10 ) do
|
171
|
+
Thread.new( req ) do |run|
|
172
|
+
req.get( "/", "HTTP_COOKIE" => cookie, 'rack.multithread' => true )
|
173
|
+
end
|
174
|
+
end.reverse.map{ |t| t.join.value }
|
175
|
+
|
176
|
+
r.each do |res|
|
177
|
+
res['Set-Cookie'].should == cookie
|
178
|
+
res.body.should include( '"counter"=>2' )
|
179
|
+
end
|
180
|
+
|
181
|
+
drop_counter = proc do |env|
|
182
|
+
env['rack.session'].delete 'counter'
|
183
|
+
env['rack.session']['foo'] = 'bar'
|
184
|
+
[200, {'Content-Type'=>'text/plain'}, env['rack.session'].inspect]
|
185
|
+
end
|
186
|
+
tses = Rack::Utils::Context.new @pool, drop_counter
|
187
|
+
treq = Rack::MockRequest.new( tses )
|
188
|
+
|
189
|
+
tnum = 10
|
190
|
+
r = Array.new(tnum) do
|
191
|
+
Thread.new(treq) do |run|
|
192
|
+
run.get('/', "HTTP_COOKIE" => cookie, 'rack.multithread' => true)
|
193
|
+
end
|
194
|
+
end.reverse.map{|t| t.join.value }
|
195
|
+
r.each do |res|
|
196
|
+
res['Set-Cookie'].should == cookie
|
197
|
+
res.body.should include('"foo"=>"bar"')
|
198
|
+
end
|
199
|
+
|
200
|
+
session = @pool.sessions.find_one( {:_id => sess_id } )
|
201
|
+
session['data'].size.should == 1
|
202
|
+
session['data']['counter'].should be_nil
|
203
|
+
session['data']['foo'].should == 'bar'
|
204
|
+
end
|
205
|
+
|
206
|
+
end
|
207
|
+
end
|