couchrest_session_store 0.1.3 → 0.2.0

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.
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: