couchrest_session_store 0.1.3 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ services:
2
+ - couchdb
3
+ notifications:
4
+ email: false
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in couchrest-session-store.gemspec
4
+ gemspec
5
+
6
+ gem 'debugger'
7
+
data/README.md CHANGED
@@ -1,7 +1,11 @@
1
- # CouchRest Session Store #
1
+ # CouchRest::Session::Store #
2
2
 
3
3
  A simple session store based on CouchRest Model.
4
4
 
5
+ It will automatically pick up the config/couch.yml file for CouchRest Model
6
+
5
7
  ## Options ##
6
8
 
7
9
  * marhal_data: (_defaults true_) - if set to false session data will be stored directly in the couch document. Otherwise it's marshalled and base64 encoded to enable restoring ruby data structures.
10
+ * database: database to use combined with config prefix and suffix
11
+ * exprire_after: livetime of a session in seconds
@@ -0,0 +1,25 @@
1
+ # _*_ encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |gem|
4
+
5
+ gem.authors = ["Azul"]
6
+ gem.email = ["azul@leap.se"]
7
+ gem.summary = "A Rails Session Store based on CouchRest Model"
8
+ gem.description = gem.summary
9
+ gem.homepage = "http://github.com/azul/couchrest_session_store"
10
+
11
+ gem.has_rdoc = true
12
+ # gem.extra_rdoc_files = ["LICENSE"]
13
+
14
+ gem.files = `git ls-files`.split("\n")
15
+ gem.name = "couchrest_session_store"
16
+ gem.require_paths = ["lib"]
17
+ gem.version = '0.2.0'
18
+
19
+ gem.add_dependency "couchrest"
20
+ gem.add_dependency "couchrest_model"
21
+ gem.add_dependency "actionpack"
22
+
23
+ gem.add_development_dependency "minitest"
24
+ gem.add_development_dependency "rake"
25
+ end
@@ -0,0 +1,76 @@
1
+ require 'couchrest/session/utility'
2
+
3
+ class CouchRest::Session::Document
4
+ include CouchRest::Session::Utility
5
+
6
+ def initialize(doc)
7
+ @doc = doc
8
+ end
9
+
10
+ def self.load(sid)
11
+ self.allocate.tap do |session_doc|
12
+ session_doc.load(sid)
13
+ end
14
+ end
15
+
16
+ def self.build(sid, session, options)
17
+ self.new(CouchRest::Document.new({"_id" => sid})).tap do |session_doc|
18
+ session_doc.update session, options
19
+ end
20
+ end
21
+
22
+ def load(sid)
23
+ @doc = database.get(sid)
24
+ end
25
+
26
+ def to_session
27
+ if doc["marshalled"]
28
+ session = unmarshal(doc["data"])
29
+ else
30
+ session = doc["data"]
31
+ end
32
+ return session
33
+ end
34
+
35
+ def delete
36
+ database.delete_doc(doc)
37
+ end
38
+
39
+ def update(session, options)
40
+ # clean up old data but leave id and revision intact
41
+ doc.reject! do |k,v|
42
+ k[0] != '_'
43
+ end
44
+ doc.merge! data_for_doc(session, options)
45
+ end
46
+
47
+ def save
48
+ database.save_doc(doc)
49
+ end
50
+
51
+ def expired?
52
+ expires && expires < Time.now
53
+ end
54
+
55
+ protected
56
+
57
+ def data_for_doc(session, options)
58
+ { "data" => options[:marshal_data] ? marshal(session) : session,
59
+ "marshalled" => options[:marshal_data],
60
+ "expires" => expiry_from_options(options) }
61
+ end
62
+
63
+ def expiry_from_options(options)
64
+ expire_after = options[:expire_after]
65
+ expire_after && (Time.now + expire_after)
66
+ end
67
+
68
+ def expires
69
+ doc["expires"] && Time.parse_iso8601(doc["expires"])
70
+ end
71
+
72
+ def doc
73
+ @doc
74
+ end
75
+
76
+ end
@@ -0,0 +1,75 @@
1
+ class CouchRest::Session::Store < ActionDispatch::Session::AbstractStore
2
+
3
+ include CouchRest::Model::Configuration
4
+ include CouchRest::Model::Connection
5
+
6
+ def initialize(app, options = {})
7
+ super
8
+ self.class.set_options(options)
9
+ end
10
+
11
+ def self.set_options(options)
12
+ @options = options
13
+ end
14
+
15
+ # just fetch from the config
16
+ def self.database
17
+ @database ||= initialize_database
18
+ end
19
+
20
+ def self.initialize_database
21
+ use_database @options[:database] || "sessions"
22
+ end
23
+
24
+ private
25
+
26
+ def get_session(env, sid)
27
+ if session = fetch_session(sid)
28
+ [sid, session]
29
+ else
30
+ [generate_sid, {}]
31
+ end
32
+ rescue RestClient::ResourceNotFound
33
+ # session data does not exist anymore
34
+ return [sid, {}]
35
+ end
36
+
37
+ def set_session(env, sid, session, options)
38
+ doc = build_or_update_doc(sid, session, options)
39
+ doc.save
40
+ return sid
41
+ end
42
+
43
+ def destroy_session(env, sid, options)
44
+ doc = secure_get(sid)
45
+ doc.delete
46
+ generate_sid unless options[:drop]
47
+ rescue RestClient::ResourceNotFound
48
+ # already destroyed - we're done.
49
+ generate_sid unless options[:drop]
50
+ end
51
+
52
+ def fetch_session(sid)
53
+ return nil unless sid
54
+ doc = secure_get(sid)
55
+ doc.to_session unless doc.expired?
56
+ end
57
+
58
+ def build_or_update_doc(sid, session, options)
59
+ options[:marshal_data] = true if options[:marshal_data].nil?
60
+ doc = secure_get(sid)
61
+ doc.update(session, options)
62
+ return doc
63
+ rescue RestClient::ResourceNotFound
64
+ CouchRest::Session::Document.build(sid, session, options)
65
+ end
66
+
67
+ # prevent access to design docs
68
+ # this should be prevented on a couch permission level as well.
69
+ # but better be save than sorry.
70
+ def secure_get(sid)
71
+ raise RestClient::ResourceNotFound if /^_design\/(.*)/ =~ sid
72
+ CouchRest::Session::Document.load(sid)
73
+ end
74
+ end
75
+
@@ -0,0 +1,16 @@
1
+ module CouchRest::Session::Utility
2
+ module_function
3
+
4
+ def marshal(data)
5
+ ::Base64.encode64(Marshal.dump(data)) if data
6
+ end
7
+
8
+ def unmarshal(data)
9
+ Marshal.load(::Base64.decode64(data)) if data
10
+ end
11
+
12
+ def database
13
+ CouchRest::Session::Store.database
14
+ end
15
+
16
+ end
@@ -0,0 +1,5 @@
1
+ module CouchRest
2
+ module Session
3
+ end
4
+ end
5
+
@@ -4,118 +4,7 @@ require 'couchrest_model'
4
4
  gem 'actionpack', '~> 3.0'
5
5
  require 'action_dispatch'
6
6
 
7
- # CouchDB session storage for Rails.
8
- #
9
- # It will automatically pick up the config/couch.yml file for CouchRest Model
10
- #
11
- # Options:
12
- # :database => database to use combined with config prefix and suffix
13
- #
14
- class CouchRestSessionStore < ActionDispatch::Session::AbstractStore
15
-
16
- include CouchRest::Model::Configuration
17
- include CouchRest::Model::Connection
18
-
19
- class << self
20
- def marshal(data)
21
- ::Base64.encode64(Marshal.dump(data)) if data
22
- end
23
-
24
- def unmarshal(data)
25
- Marshal.load(::Base64.decode64(data)) if data
26
- end
27
-
28
- end
29
-
30
- def initialize(app, options = {})
31
- super
32
- self.class.set_options(options)
33
- end
34
-
35
- def self.set_options(options)
36
- @options = options
37
- end
38
-
39
- # just fetch from the config
40
- def self.database
41
- @database ||= initialize_database
42
- end
43
-
44
- def self.initialize_database
45
- use_database @options[:database] || "sessions"
46
- end
47
-
48
- def database
49
- self.class.database
50
- end
51
-
52
- private
53
-
54
- def get_session(env, sid)
55
- if sid
56
- doc = secure_get(sid)
57
- if doc["not_marshalled"]
58
- session = doc.to_hash
59
- session.delete("not_marshalled")
60
- else
61
- session = self.class.unmarshal(doc["data"])
62
- end
63
- [sid, session]
64
- else
65
- [generate_sid, {}]
66
- end
67
- rescue RestClient::ResourceNotFound
68
- # session data does not exist anymore
69
- return [sid, {}]
70
- end
71
-
72
- def set_session(env, sid, session, options)
73
- doc = build_or_update_doc(sid, session, options[:marshal_data])
74
- database.save_doc(doc)
75
- return sid
76
- end
77
-
78
- def destroy_session(env, sid, options)
79
- doc = secure_get(sid)
80
- database.delete_doc(doc)
81
- generate_sid unless options[:drop]
82
- rescue RestClient::ResourceNotFound
83
- # already destroyed - we're done.
84
- generate_sid unless options[:drop]
85
- end
86
-
87
- def build_or_update_doc(sid, session, marshal_data)
88
- marshal_data = true if marshal_data.nil?
89
- doc = secure_get(sid)
90
- update_doc doc, data_for_doc(session, marshal_data)
91
- return doc
92
- rescue RestClient::ResourceNotFound
93
- data = data_for_doc(session, marshal_data).merge({"_id" => sid})
94
- return CouchRest::Document.new(data)
95
- end
96
-
97
- def update_doc(doc, data)
98
- # clean up old data but leave id and revision intact
99
- doc.reject! do |k,v|
100
- k[0] != '_'
101
- end
102
- doc.merge! data
103
- end
104
-
105
- def data_for_doc(session, marshal_data)
106
- if marshal_data
107
- { "data" => self.class.marshal(session) }
108
- else
109
- session.merge({"not_marshalled" => true})
110
- end
111
- end
112
-
113
- # prevent access to design docs
114
- # this should be prevented on a couch permission level as well.
115
- # but better be save than sorry.
116
- def secure_get(sid)
117
- raise RestClient::ResourceNotFound if /^_design\/(.*)/ =~ sid
118
- database.get(sid)
119
- end
120
- end
7
+ require 'couchrest/session'
8
+ require 'couchrest/session/store'
9
+ require 'couchrest/session/document'
121
10
 
@@ -0,0 +1,26 @@
1
+ #
2
+ # Access the couch directly so we can test its state without relying
3
+ # on the SessionStore
4
+ #
5
+
6
+ class CouchTester
7
+ include CouchRest::Model::Configuration
8
+ include CouchRest::Model::Connection
9
+
10
+ attr_reader :database
11
+
12
+ def initialize(options = {})
13
+ @database = self.class.use_database options[:database] || "sessions"
14
+ end
15
+
16
+ def get(sid)
17
+ database.get(sid)
18
+ end
19
+
20
+ def update(sid, diff)
21
+ doc = database.get(sid)
22
+ doc.merge! diff
23
+ database.save_doc(doc)
24
+ end
25
+
26
+ end
@@ -0,0 +1,111 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/test_helper')
2
+
3
+ class SessionStoreTest < MiniTest::Test
4
+
5
+ def test_session_initialization
6
+ sid, session = store.send :get_session, env, nil
7
+ assert sid
8
+ assert_equal Hash.new, session
9
+ end
10
+
11
+ def test_normal_session_flow
12
+ sid, session = init_session
13
+ store_session(sid, session)
14
+ assert_equal [sid, session], store.send(:get_session, env, sid)
15
+ store.send :destroy_session, env, sid, {}
16
+ end
17
+
18
+ def test_updating_session
19
+ sid, session = init_session
20
+ store_session(sid, session)
21
+ session[:bla] = "blub"
22
+ store.send :set_session, env, sid, session, {}
23
+ assert_equal [sid, session], store.send(:get_session, env, sid)
24
+ store.send :destroy_session, env, sid, {}
25
+ end
26
+
27
+ def test_unmarshalled_session_flow
28
+ sid, session = init_session
29
+ store_session sid, session, :marshal_data => false
30
+ new_sid, new_session = store.send(:get_session, env, sid)
31
+ assert_equal sid, new_sid
32
+ assert_equal session[:key], new_session["key"]
33
+ store.send :destroy_session, env, sid, {}
34
+ end
35
+
36
+ def test_unmarshalled_data
37
+ sid, session = init_session
38
+ store_session sid, session, :marshal_data => false
39
+ couch = CouchTester.new
40
+ data = couch.get(sid)["data"]
41
+ assert_equal session[:key], data["key"]
42
+ end
43
+
44
+ def test_logout_in_between
45
+ sid, session = init_session
46
+ store_session(sid, session)
47
+ store.send :destroy_session, env, sid, {}
48
+ other_sid, other_session = store.send(:get_session, env, sid)
49
+ assert_equal Hash.new, other_session
50
+ end
51
+
52
+ def test_can_logout_twice
53
+ sid, session = init_session
54
+ store_session(sid, session)
55
+ store.send :destroy_session, env, sid, {}
56
+ store.send :destroy_session, env, sid, {}
57
+ other_sid, other_session = store.send(:get_session, env, sid)
58
+ assert_equal Hash.new, other_session
59
+ end
60
+
61
+ def test_stored_and_not_expired_yet
62
+ sid, session = init_session
63
+ store_session(sid, session, expire_after: 300)
64
+ doc = CouchRest::Session::Document.load(sid)
65
+ expires = doc.send :expires
66
+ assert expires
67
+ assert !doc.expired?
68
+ assert (expires - Time.now) > 0, "Exiry should be in the future"
69
+ assert (expires - Time.now) <= 300, "Should expire after 300 seconds - not more"
70
+ assert_equal [sid, session], store.send(:get_session, env, sid)
71
+ end
72
+
73
+ def test_stored_but_expired
74
+ sid, session = init_session
75
+ store_session(sid, session, expire_after: 300)
76
+ CouchTester.new.update(sid, "expires" => Time.now - 2.minutes)
77
+ other_sid, other_session = store.send(:get_session, env, sid)
78
+ assert_equal Hash.new, other_session, "session should have expired"
79
+ assert other_sid != sid
80
+ end
81
+
82
+ def test_store_without_expiry
83
+ sid, session = init_session
84
+ store_session(sid, session)
85
+ couch = CouchTester.new
86
+ assert_nil couch.get(sid)["expires"]
87
+ assert_equal [sid, session], store.send(:get_session, env, sid)
88
+ end
89
+
90
+ def app
91
+ nil
92
+ end
93
+
94
+ def store(options = {})
95
+ @store ||= CouchRest::Session::Store.new(app, options)
96
+ end
97
+
98
+ def env(settings = {})
99
+ env ||= settings
100
+ end
101
+
102
+ def init_session
103
+ sid, session = store.send :get_session, env, nil
104
+ session[:key] = "stub"
105
+ return sid, session
106
+ end
107
+
108
+ def store_session(sid, session, options = {})
109
+ store.send :set_session, env, sid, session, options
110
+ end
111
+ end
@@ -0,0 +1,12 @@
1
+ class TestClock
2
+ attr_accessor :now
3
+
4
+ def initialize(tick = 60)
5
+ @tick = tick
6
+ @now = Time.now
7
+ end
8
+
9
+ def tick(seconds = nil)
10
+ @now += seconds || @tick
11
+ end
12
+ end
@@ -0,0 +1,6 @@
1
+ require "rubygems"
2
+ gem 'minitest'
3
+ require 'minitest/autorun'
4
+ require File.expand_path(File.dirname(__FILE__) + '/../lib/couchrest_session_store.rb')
5
+ require File.expand_path(File.dirname(__FILE__) + '/couch_tester.rb')
6
+ require File.expand_path(File.dirname(__FILE__) + '/test_clock.rb')
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: couchrest_session_store
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.2.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-07-17 00:00:00.000000000 Z
12
+ date: 2013-09-17 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: couchrest
@@ -98,9 +98,20 @@ executables: []
98
98
  extensions: []
99
99
  extra_rdoc_files: []
100
100
  files:
101
+ - .travis.yml
102
+ - Gemfile
101
103
  - README.md
102
104
  - Rakefile
105
+ - couchrest_session_store.gemspec
106
+ - lib/couchrest/session.rb
107
+ - lib/couchrest/session/document.rb
108
+ - lib/couchrest/session/store.rb
109
+ - lib/couchrest/session/utility.rb
103
110
  - lib/couchrest_session_store.rb
111
+ - test/couch_tester.rb
112
+ - test/session_store_test.rb
113
+ - test/test_clock.rb
114
+ - test/test_helper.rb
104
115
  homepage: http://github.com/azul/couchrest_session_store
105
116
  licenses: []
106
117
  post_install_message: